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>
<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><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>
<div id="steps">
<section id="bisrvSection">
<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>
<div id="bisrvOutput"></div>
<div id="bisrvMessages"></div>
<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>
</div>
</section>
</div>
<script src="tools.js"></script>
<script type="text/javascript">
// Global variables...
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) {
// We've got a new file - clear any old error messages and any HTML after
// Step 1...
document.getElementById("bisrvOutput").innerHTML = "";
document.getElementById("bisrvMessages").innerHTML = "";
while(document.getElementById("bisrvSection").nextSibling) {
document.getElementById("bisrvSection").nextSibling.remove();
}
@ -36,12 +41,12 @@
// Check to make sure the file is at least ~512-bytes-ish long (CRC32 for
// bisrv.asd is calculated for bytes 512 onwards)...
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;
}
// 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();
frBisrv.readAsArrayBuffer(file);
frBisrv.onload = function(event) {
@ -49,6 +54,9 @@
// Read the provided file's data into an array...
bisrvData = new Uint8Array(event.target.result);
// Patch the CRC32 bytes...
patchCRC32(bisrvData);
// And display Step 2...
stepTwo();
};
@ -68,47 +76,11 @@
// Attach our event handler to our download button...
var dButton = document.getElementById("downloadButton");
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>
<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>
</html>

View File

@ -10,31 +10,17 @@
<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>
<hr>
<section id="bisrvSection">
<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>
<form id="bisrvForm" action="#">
<label>Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" onchange="bisrvLoad(event.target.files[0])"></label>
</form>
<div id="bisrvOutput"></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>
<div id="steps">
<section id="bisrvSection">
<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>
<div id="bisrvMessages"></div>
<div class="controlContainer">
<label class="control">Open <code>bisrv.asd</code>: <input id="bisrvSelector" type="file" accept=".asd" onchange="bisrvLoad(event.target.files[0])"></label>
</div>
</section>
</div>
<script src="tools.js"></script>
<script type="text/javascript">
// Global variables...
@ -42,305 +28,198 @@
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
// 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 triggered when the person selects a file in Step 1...
function bisrvLoad(file) {
// Read in the contents of the selected file as array buffer...
var frBisrv = new FileReader();
frBisrv.readAsArrayBuffer(file);
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...
var data = new Uint8Array(event.target.result);
bisrvData = new Uint8Array(event.target.result);
// 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
// a hash, or false otherwise... let's check!
if (hashResult instanceof Promise) {
// We got a Promise! Wait for it to finish so we get our bisrv.asd hash...
hashResult.then(function(dataHash) {
// Check the hash against all the known good ones...
switch (dataHash) {
switch (knownHash(dataHash)) {
// Mid-March BIOS...
case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1":
case "03.15":
logoOffset = 0x9B9030;
bisrvData = data;
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: Mid-March <code>bisrv.asd</code> detected</p>";
setMessage("info", "bisrvMessages", "INFO: Mid-March <code>bisrv.asd</code> detected.");
break;
// April 20th BIOS...
case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca":
case "04.20":
logoOffset = 0x9B91D8;
bisrvData = data;
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: April 20th <code>bisrv.asd</code> detected</p>";
setMessage("info", "bisrvMessages", "INFO: April 20th <code>bisrv.asd</code> detected.");
break;
// May 15th BIOS...
case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd":
case "05.15":
logoOffset = 0x9BB0B8;
bisrvData = data;
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 15th bisrv.asd detected</p>";
setMessage("info", "bisrvMessages", "INFO: May 15th <code>bisrv.asd</code> detected.");
break;
// May 22nd BIOS...
case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07":
case "05.22":
logoOffset = 0x9BB098;
bisrvData = data;
document.getElementById("bisrvOutput").innerHTML = "<p class=\"infoMessage\">INFO: May 22nd bisrv.asd detected</p>";
setMessage("info", "bisrvMessages", "INFO: May 22nd <code>bisrv.asd</code> detected.");
break;
default:
// Huh... wasn't false so had bisrv.asd structure, but didn't return
// a known hash... a new BIOS version? Unknown anyway!
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;
break;
}
// If we're here we've got a good file, so enable the input for step 2 (image selection)...
document.getElementById("imageSelector").removeAttribute("disabled");
// If we're here we've got a good file, so onwards to Step 2 (image selection)...
stepTwo();
});
}
else {
// 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;
}
};
}
function imageLoad(file) {
var frImage = new FileReader();
// This function triggers when the user has selected a known `bisrv.asd`
// variant...
function stepTwo() {
frImage.onload = function(event) {
// 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>";
// First check to make sure the selected file's data URL includes a PNG or JPEG data type...
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", "");
return;
// 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();
}
// Create an image and set its src to our data URL; this triggers the onload event if
// it's a valid image file...
var img = new Image;
img.src = event.target.result;
img.onload = function() {
// Check to make sure the image has the right dimensions for the boot logo...
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>";
document.getElementById("downloadButton").setAttribute("disabled", "");
// Now let's read in the file...
var frImage = new FileReader();
frImage.readAsDataURL(event.target.files[0]);
frImage.onload = function(event) {
// 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;")) {
// 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;
}
// Now we're going to load the file's data into an Image object, so we can
// access the raw image data...
var img = new Image;
img.src = event.target.result;
img.onload = function(event) {
// Create a virtual canvas, and load it up with our image file...
var canv = document.createElement("canvas");
var cont = canv.getContext("2d");
cont.canvas.width = 512;
cont.canvas.height = 200;
// Draw our image to the canvas, which will allow us to get data about the image...
cont.drawImage(img, 0, 0, 256, 100);
var data = cont.getImageData(0, 0, 256, 100).data;
// Now we're going to scale up the image to 512x200 (the internal image size used
// within the bios); we do this manually to get "nearest neighbour" scaling...
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);
// Check to make sure the image has the correct dimensions for a
// boot logo...
if (img.width != 256 || img.height != 100) {
setMessage("error", "imageMessages", "ERROR: The selected image does not have dimensions of 256x100px!");
return;
}
// We need to scale the image to the internal resolution used by
// the SF2000 (512x200)...
var imageData = scaleImage(imageToImageData(img), 512, 200, "Nearest Neighbour");
// Next we'll convert the image data to little-endian RGB565
// format for the SF2000...
newLogoData = imageDataToRgb565(imageData);
// On to Step 3!
stepThree();
}
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
// 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 assign to our global...
newLogoData = new Uint8Array(buffer);
// We should be all done with this step; enable the download button and give the
// user some UI feedback...
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);
});
}
function download() {
// 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
// our new logo's binary data in newLogoData. All we need to do is replace the old
// data bytes with the new bytes, re-calculate the CRC32 bytes for the modified
// file and set them in the data, and send the data to the user's browser. Easy!
function stepThree() {
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>";
// First, replace the logo data...
for (var i = 0; i < newLogoData.length; i++) {
bisrvData[logoOffset + i] = newLogoData[i];
}
// Add our download button...
html += "<div class=\"controlContainer\"><div class=\"control\"><input id=\"userDownload\" type=\"button\" value=\"Download\"></div></div>";
// Next, we 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;
// 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);
// Let's add the event handler for our Download button...
var dButton = document.getElementById("userDownload");
dButton.addEventListener("click", function() {
// 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
// our new logo's binary data in newLogoData. All we need to do is replace the old
// data bytes with the new bytes, re-calculate the CRC32 bytes for the modified
// file and set them in the data, and send the data to the user's browser. Easy!
// First, replace the logo data...
for (var i = 0; i < newLogoData.length; i++) {
bisrvData[logoOffset + i] = newLogoData[i];
}
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...
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);
// Next, we calculate a new CRC32 for the updated bisrv.asd and apply it...
patchCRC32(bisrvData);
// And finally, send the data to the user's browser as a file download...
downloadToBrowser(bisrvData, "application/octet-stream", "bisrv.asd");
});
}
</script>
<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>
</html>

View File

@ -16,17 +16,19 @@
<section id="fileSection">
<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>
<form id="fileForm" action="#">
<label>Open file: <input id="fileSelector" type="file" onchange="fileLoad(event.target.files[0])"></label>
</form>
<div id="fileOutput"></div>
<div id="fileMessages"></div>
<div class="controlContainer">
<label class="control">Open file: <input id="fileSelector" type="file" onchange="fileLoad(event.target.files[0])"></label>
</div>
</section>
</div>
<script src="tools.js"></script>
<script>
// Global variables...
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 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 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 };
}
// 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...
function fileLoad(file) {
@ -156,21 +56,19 @@
// an array buffer...
var fr = new FileReader();
fr.readAsArrayBuffer(file);
// Triggered when the FileReader reads the file's contents...
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...
while(document.getElementById("fileSection").nextSibling) {
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...
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
// a hash, or false otherwise... let's check!
if (hashResult instanceof Promise) {
// We got a Promise! Wait for it to finish so we get our bisrv.asd hash...
hashResult.then(function(dataHash) {
// 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...
var step1BRequired = false;
// Check the hash against all the known good ones...
switch (dataHash) {
firmwareVersion = knownHash(dataHash);
switch (firmwareVersion) {
// Mid-March BIOS...
case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1":
case "03.15":
mappingTableOffset = 0x8DBC0C;
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;
// April 20th BIOS...
case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca":
case "04.20":
mappingTableOffset = 0x8DBC9C;
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;
// May 15th BIOS...
case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd":
case "05.15":
mappingTableOffset = 0;
mappingConsoles = ["NES", "Genesis/Mega Drive, Master System", "SNES", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"];
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;
// May 22nd BIOS...
case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07":
case "05.22":
mappingTableOffset = 0;
mappingConsoles = ["NES", "SNES", "Genesis/Mega Drive, Master System", "Game Boy, Game Boy Color", "Game Boy Advance", "Arcade"];
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;
default:
// Huh... wasn't false so had bisrv.asd structure, but didn't return
// a known hash... a new BIOS version? Unknown anyway!
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;
break;
}
@ -252,7 +153,7 @@
if (data.length == 288) {
// 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...
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;
}
// If we're still checking, next test the file extensions for the individual console's ROMs...
@ -283,7 +184,7 @@
else {
// Oh dear, the provided file didn't match any of the above rules! Display an error
// 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;
}
@ -293,12 +194,14 @@
// and we'll set mappingData to it's full contents instead...
if (mappingConsoles.length == 1) {
mappingData = new Uint8Array(48);
mappingTableOffset = 0;
}
else {
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...
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() {
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", html);
// Attach our event handler to our new file input control...
var keyMapInfoInput = document.getElementById("keyMapInfoSelector");
keyMapInfoInput.addEventListener("change", function() {
// The user has chosen a new file; it should be a KeyMapInfo.kmp file,
// 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();
fileName = event.target.files[0].name;
frKMI.readAsDataURL(event.target.files[0]);
frKMI.onload = function(event) {
// Get the file's data and data type...
var fileData = event.target.result;
var dataType = fileData.substring(5, fileData.indexOf(";"));
// Check to make sure the data type is binary...
if (dataType === "application/octet-stream") {
// The user selected a file that appears to contain binary data; it's
// a good candidate! Let's check its length next...
var base64Data = fileData.substring(fileData.indexOf(",") + 1);
var binaryData = atob(base64Data);
if (binaryData.length == 288) {
// 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);
for (var i = 0; i < binaryData.length; i++) {
mappingData[i] = binaryData.charCodeAt(i);
}
while(document.getElementById("stepOneB").nextSibling) {
document.getElementById("stepOneB").nextSibling.remove();
}
stepTwo();
}
else {
// 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;
}
}
else {
// The file the user selected doesn't appear to be binary data, so
// 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;
}
}
});
}
// This function triggers automatically when the user selects a valid
// file (or files) in Step 1...
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
// which console it's for, and then a section each for Player 1 and Player 2. Each
// player section will have a list of the six SF2000 buttons that are available to be
// 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.
// 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 which console it's for, and then
// a section each for Player 1 and Player 2. Each player section will
// have a list of the six SF2000 buttons that are available to be
// 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>";
// First, we need to update Step 2's instructions, depending on whether or not the user
// supplied a bisrv.asd file or KeyMapInfo.kmp (multiple consoles) or a ROM (one console)...
// First, we need to update Step 2's instructions, depending on whether
// or not the user supplied a bisrv.asd file or KeyMapInfo.kmp
// (multiple consoles) or a ROM (one console)...
if (mappingConsoles.length > 1) {
// 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>";
}
else {
// 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 += "<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'];
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
// a <h3> header for good measure as well...
html += "<div class=\"mappingConsole\"><h3>" + mappingConsoles[currentConsole] + "</h3>";
// This console's bank of mapping controls will be stored in a <div>,
// and we'll add a <h3> header for good measure as well...
html += "<div class=\"control\"><h3>" + mappingConsoles[currentConsole] + "</h3>";
// Get the button mapping for this console...
var buttonMap = getButtonMap(currentConsole);
// We'll add two tables of control mappings to the <div>, one each for Player 1 and
// Player 2...
// We'll add two tables of control mappings to the <div>, one each
// for Player 1 and Player 2...
for (var player = 0; player < 2; player++) {
// 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 += "<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++) {
// 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.
// We specify the order the bytes are specified in here. If they do other weird
// stuff in the future, it'll probably be here that needs to change!
// By default, the SF2000 stores its button maps in XYLABR
// order... except for GBA under newer firmware versions where
// 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'];
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'];
}
// 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);
// Start creating the HTML data for this row in the table...
@ -458,135 +396,132 @@
// 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", html);
while(document.getElementById("mappingSection").nextSibling) {
document.getElementById("mappingSection").nextSibling.remove();
}
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
// the per-rom .kmp file), as well as generate a button that (when clicked) will
// 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 the per-rom .kmp
// file), as well as generate a button that (when clicked) will
// 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>";
// First up, instructions! These will depend on whether they provided a bisrv.asd,
// a KeyMapInfo.kmp, or a game ROM...
// First up, instructions! These will depend on whether they provided a
// bisrv.asd, a KeyMapInfo.kmp, or a game ROM...
if (mappingConsoles.length > 1) {
if (fileName == "bisrv.asd") {
// 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>";
}
else if (fileName == "KeyMapInfo.kmp") {
// 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>";
}
else {
// 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>";
}
}
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';});
// 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>";
}
// Now let's add the Download button with it's event...
html += "<form id=\"downloadForm\" action=\"#\"><input id=\"downloadButton\" type=\"button\" value=\"Download\" onclick=\"download()\"></form>";
// Add our download button...
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", html);
while(document.getElementById("saveSection").nextSibling) {
document.getElementById("saveSection").nextSibling.remove();
}
}
function download() {
// Here, we'll construct the file for the user to download (a modified
// bisrv.asd, a modified KeyMapInfo.kmp, or a .kmp keymap file), and send
// it to the user's browser...
// Let's add the event handler for our Download button...
var dButton = document.getElementById("downloadButton");
dButton.addEventListener("click", function() {
// 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'];
for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole ++) {
// Here, we'll construct the file for the user to download (a
// modified bisrv.asd, a modified KeyMapInfo.kmp, or a .kmp keymap
// file), and send it to the user's browser...
// Get the button mapping for this console...
var buttonMap = getButtonMap(currentConsole);
// 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'];
for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole ++) {
// For each player...
for (var player = 0; player < 2; player++) {
// ... and for each 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.
// We specify the order the bytes are specified 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'];
if (mappingConsoles[currentConsole] == "Game Boy Advance" && fileName == "KeyMapInfo.kmp") {
buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y'];
// Get the button mapping for this console...
var buttonMap = getButtonMap(currentConsole);
// For each player...
for (var player = 0; player < 2; player++) {
// ... and for each button...
for (var button = 0; button < 6; button++) {
// By default, the SF2000 stores its button maps in XYLABR
// order... except for GBA under newer firmware versions where
// 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'];
if (mappingConsoles[currentConsole] == "Game Boy Advance" && ["05.15", "05.22"].includes(firmwareVersion)) {
buttonByteOrder = ['L', 'R', 'X', 'A', 'B', 'Y'];
}
// Calculate the offset in our mapping data for the current
// button, read the button settings from the HTML controls, and
// assign the appropriate values to our binary mappingData...
var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4);
mappingData[offset] = buttonMap[document.getElementById("sel" + offset.toString(16)).value];
mappingData[offset + 2] = document.getElementById("cb" + offset.toString(16)).checked ? 1 : 0;
}
// Calculate the offset in our mapping data for the current button, read
// the button settings from the HTML controls, and assign the appropriate
// values to our binary mappingData...
var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (buttonByteOrder.indexOf(presentationButtonOrder[button]) * 4);
mappingData[offset] = buttonMap[document.getElementById("sel" + offset.toString(16)).value];
mappingData[offset + 2] = document.getElementById("cb" + offset.toString(16)).checked ? 1 : 0;
}
}
}
// Now that we've got our updated data, we'll need to check if it's an updated
// bisrv.asd or not - if it is, we'll need to update some CRC32 check-bits
// in the bisrv.asd data as well...
if (mappingConsoles.length > 1 && fileName == "bisrv.asd") {
// It's a bisrv.asd alright! Let's do the CRC32 update dance...
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;
// Now that we've got our updated data, we'll need to check if it's
// an updated bisrv.asd or not - if it is, we'll need to update some
// CRC32 check-bits in the bisrv.asd data as well...
if (mappingConsoles.length > 1 && fileName == "bisrv.asd") {
// It's a bisrv.asd alright! Let's do the CRC32 update dance...
patchCRC32(mappingData);
}
c = ~0;
for (var i = 512; i < mappingData.length; i++) {
c = c << 8 ^ tabCRC32[c >>> 24 ^ mappingData[i]];
// Next, let's determine the name of the file we're going to send to
// the user's browser...
var downloadFileName = fileName;
if (mappingConsoles.length == 1) {
downloadFileName = downloadFileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';});
}
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
// to the user's browser...
var downloadFileName = fileName;
if (mappingConsoles.length == 1) {
downloadFileName = downloadFileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';});
}
// And finally, send the data to the user's browser as a download...
downloadToBrowser(mappingData, "application/octet-stream", downloadFileName);
// Finally, send the file!
var link = document.createElement("a");
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>
<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>
</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>
<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 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>
<div id="steps">
<section id="modeSection">
@ -23,6 +23,7 @@
</section>
</div>
<script src="pako.js"></script>
<script src="tools.js"></script>
<script>
// Global Variables
@ -36,10 +37,8 @@
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
// The following variable contains a definition for an SVG image to be used
// in any error messages shown to the user; defining it once so it can be
// reused more easily...
var errorIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAgNjQgNjQnPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0nYic+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZjU4MTYyJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjZTI1ZjUzJy8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9J2EnPjxzdG9wIG9mZnNldD0nMCcgc3RvcC1jb2xvcj0nI2VjODE2YycvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nI2JmNDMyOScvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHhsaW5rOmhyZWY9JyNhJyBpZD0nYycgeDE9JzEyJyB4Mj0nNTInIHkxPScxMicgeTI9JzUyJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPScjYicgaWQ9J2QnIHgxPScyMCcgeDI9JzQ4JyB5MT0nMTYnIHkyPSc0NCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnLz48L2RlZnM+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjgnIGZpbGw9J3VybCgjYyknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjQnIGZpbGw9JyNmOWQzY2MnIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PGNpcmNsZSBjeD0nMzInIGN5PSczMicgcj0nMjAnIGZpbGw9J3VybCgjZCknIHBhaW50LW9yZGVyPSdtYXJrZXJzIHN0cm9rZSBmaWxsJy8+PHBhdGggZmlsbD0nI2ZmZicgZD0nTTI5IDE2djIwaDZWMTZaTTI5IDQwdjZoNnYtNnonIHBhaW50LW9yZGVyPSdmaWxsIG1hcmtlcnMgc3Ryb2tlJy8+PC9zdmc+";
// Display our initial warning note...
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!");
// 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
@ -108,14 +107,16 @@
// save and thumbnail data... otherwise, it returns false and we let
// the user know the file didn't appear to be valid...
if (validateSave(new Uint8Array(event.target.result))) {
// Great, the user selected a valid SF2000 format save state file!
// On to Step 3!
setupStepThree_From();
}
else {
// Whoops, the file doesn't looke like it was an SF2000 save state file.
// 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;
}
}
@ -160,9 +161,10 @@
// 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)...
if (!event.target.result.includes("application/octet-stream")) {
// Whoops! Doesn't look like the user provided a binary file, so can't be
// 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;
}
@ -182,7 +184,7 @@
// On to Step 3!
setupStepThree_To();
}
})
});
}
// This function sets up the HTML for "Convert from SF2000 > Step 2 > Step 3"...
@ -222,19 +224,13 @@
if (match) {
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
// our initial preview canvas state, and perform our initial conversion
// and rendering...
var canvas = document.getElementById("thumbnailPreview");
var context = canvas.getContext("2d");
canvas.width = thumbWidth;
canvas.height = thumbHeight;
convertImageFromSF2000();
renderImageToCanvas();
// The last thing to do here in Step 3 is to perform our initial image
// data conversion and preview rendering... so let's do it!
convertFromSF2000AndRender();
}
// This function sets up the HTML for "Convert to SF2000 > Step 2 > Step 3"...
@ -305,7 +301,7 @@
userStateDownloadCheck();
// 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;
}
else {
@ -319,7 +315,7 @@
}
}
})
});
// Next up, our radio buttons...
var slots = document.getElementsByName("saveSlot");
@ -329,7 +325,7 @@
// state-checking function; we'll pull the actual data from the
// selected radio button at run time during the download process...
userStateDownloadCheck();
})
});
}
// And last but not least, our Download button...
@ -345,13 +341,13 @@
// Oh! We failed to compress the save state data using the pako
// library for some reason... 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: 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;
}
// We also need to check our compressed data is of an appropriate size...
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;
}
@ -424,7 +420,7 @@
// Just as a final sanity check, offset should now be equal to
// saveData.length... let's confirm!
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;
}
@ -444,7 +440,7 @@
downloadName = saveFileName + ".sa" + document.querySelector('input[name="saveSlot"]:checked').value;
// Right, that should be everything we need... to the Downloadmobile!
downloadSaveState();
downloadToBrowser(saveData, "application/octet-stream", downloadName);
});
}
@ -567,7 +563,6 @@
return true;
}
// 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...
// 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
// the button is disabled...
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!
var dButton = document.getElementById("saveStateDownload");
dButton.removeAttribute("disabled");
@ -666,96 +660,20 @@
}
}
// This function takes the SF2000-format binary image data stored in our
// thumbData global variable, and converts it to 8-bit RGB(A) format,
// storing the result in our previewCanvasData global. This is then used
// for drawing the image data to our preview canvas...
function convertImageFromSF2000() {
// 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...
// This function takes our global thumbData object (Uint8Array), converts
// it to an ImageData object and assigns it to our previewCanvasData
// global; it then renders that ImageData to our preview canvas...
function convertFromSF2000AndRender() {
previewCanvasData = rgb565ToImageData(thumbData, thumbWidth, thumbHeight);
var canvas = document.getElementById("thumbnailPreview");
var context = canvas.getContext("2d");
// Now, let's start drawing stuff to our canvas; we'll loop through
// every row and column...
for (var y = 0; y < canvas.height; y++) {
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);
canvas.width = previewCanvasData.width;
canvas.height = previewCanvasData.height;
context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(previewCanvasData, 0, 0);
}
</script>
<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>
</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;
}