<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 <ahref="https://github.com/nodeca/pako">pako</a> JavaScript library.</p>
<pclass="warning"><imgclass="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 <ahref="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>
<hr>
<divid="steps">
<sectionid="modeSection">
<h2>Step 1: Select Operating Mode</h2>
<p>This tool can operate in two main modes - you can use it to convert save states <em>from</em> the SF2000 format <em>to</em> raw save-state data (and optionally download the thumbnail), or you can use it to convert raw save state data <em>from</em> another emulator <em>to</em> the SF2000 format. Choose the mode you want, and follow the next instruction that appears.</p>
var previewCanvasData; // Binary data used for drawing to our preview canvas
var saveFileName; // Name of the save state file chosen by the user
var downloadName; // Name we're going to give any download being sent to the user
var saveData; // Binary data of a save state file
var thumbData; // Binary data of a save state thumbnail
var thumbWidth; // Integer width of a save state thumbnail
var thumbHeight; // Integer height of a save state thumbnail
var romFileGood; // Boolean indicating if a chosen save state file appears to be binary or not
// 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+";
// 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
// rewriting the rest of the page...
var modes = document.getElementsByName("toolMode");
for (var i = 0; i <modes.length;i++){
modes[i].addEventListener("change", function() {
// Clear out any HTML that might already exist after Step 1, just so
// things are nice and clean for Step 2 to load into...
// Add the appropriate Step 2 depending on what was selected...
if (this.value == "fromSF2000") {
setupStepTwo_From();
}
else if (this.value == "toSF2000") {
setupStepTwo_To();
}
});
}
// This function sets up the HTML for "Convert from SF2000 > Step 2", selecting
// a save state file from the SF2000's microSD card that the user wants to
// convert to raw save state data and/or a thumbnail...
function setupStepTwo_From() {
// Create the new section, add our heading and our instruction paragraph...
var html = "<sectionid=\"selectSF2000File\"><h2>Step 2: Select SF2000 Save State File</h2><p>Select the SF2000 save state file you want to convert from. The save states are usually stored in the <code>save</code> subfolder where the ROM for the game is located. There can be up to four saves states per game, with the extensions <code>.sa0</code>, <code>.sa1</code>, <code>.sa2</code> and <code>.sa3</code> respectively.</p><divid=\"step2Messages\"></div>";
// Add our file chooser...
html += "<divclass=\"controlContainer\"><labelclass=\"control\"><inputid=\"inputSAFile\"type=\"file\"accept=\".sa0,.sa1,.sa2,.sa3,.skp,application/octet-stream\"></label></div>";
// Close off our section...
html += "</section>";
// Finally, add a <hr> separator after the last step, and append the new step...
// Now let's read in our file data as a binary stream...
var frSave = new FileReader();
saveFileName = event.target.files[0].name;
frSave.readAsArrayBuffer(event.target.files[0]);
frSave.onload = function(event) {
// Let's run the file's data through our validation function; if the
// function returns true, then the file was a valid SF2000 save
// state file, and our global variables have been updated with the
// 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 = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"aria-hidden=\"true\"src=\""+errorIcon+"\"> ERROR: The selected file does not appear to be a valid SF2000 save state file.</p>";
return;
}
}
});
}
// This function sets up the HTML for "Convert to SF2000 > Step 2", selecting
// the non-SF2000 save state file the user wants to convert to an SF2000 save
// state bundle...
function setupStepTwo_To() {
// Start our new section, add our header and our instructions...
var html = "<sectionid=\"selectStateFile\"><h2>Step 2: Save State File</h2><p>Select the save state file from your non-SF2000 emulator that you want to convert to an SF2000-compatible save state bundle.</p><divid=\"step2Messages\"></div>";
// Add our file chooser...
html += "<divclass=\"controlContainer\"><labelclass=\"control\"><inputid=\"inputStateFile\"type=\"file\"></label></div>";
// Close our section...
html += "</section>";
// Finally, add a <hr> separator after the last step, and append the new step...
// 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 = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"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>";
return;
}
// If we're here then it was a binary file; read the data into our global
// saveData buffer...
var base64Data = event.target.result.substring(event.target.result.indexOf(",") + 1);
// This function sets up the HTML for "Convert from SF2000 > Step 2 > Step 3"...
function setupStepThree_From() {
// In this section, we'll be presenting the user with the thumbnail of
// their save state, and a download button for downloading the save
// state data itself. Let's start building our HTML...
var html = "<sectionid=\"sf2000StateDownload\"><h2>Step 3: Download Save State Data</h2><p>Below you'll find the thumbnail for your chosen save state file, as well as a download button which will let you download the raw save state data for use with other emulators/devices. If you want to save the thumbnail as well, just right-click or tap-and-hold on it like any other image, and save it that way.</p>";
// Next, we'll add the image preview...
html += "<divclass=\"controlContainer\"><divclass=\"control\"><canvasid=\"thumbnailPreview\"></canvas></div></div>";
// ... and the Download button...
html += "<divclass=\"controlContainer\"><divclass=\"control\"><inputid=\"saveStateDownload\"type=\"button\"value=\"Download\"></div></div>";
// ... and lastly we'll close off our section...
html += "</section>";
// Add a <hr> separator after the previous step, and append the new step...
var canvas = document.getElementById("thumbnailPreview");
var context = canvas.getContext("2d");
canvas.width = thumbWidth;
canvas.height = thumbHeight;
convertImageFromSF2000();
renderImageToCanvas();
}
// This function sets up the HTML for "Convert to SF2000 > Step 2 > Step 3"...
function setupStepThree_To() {
// In this section, we'll be rendering the HTML for some controls - the
// user needs to select the ROM file their new save state is for (so we
// can name the save state file correctly), and choose whether their new
// save state should be stored in slot 1, 2, 3 or 4 (again, required for
// the file name). Let's start by creating our new section, and add its
// heading and instruction paragraph...
html = "<sectionid=\"processUserFile\"><h2>Step 3: Select Game ROM And Slot Number</h2><p>To name your new SF2000 save state properly, this tool needs to know two things - the name of the game ROM file the save state is for, and which save slot (1, 2, 3 or 4) the save state should take up. Use the controls below to select your game ROM file from your SF2000's microSD card, and to choose which save slot you want.</p><p>When you've chosen a ROM file and a slot number, the Download button will become enabled, and can be used to download your new save state file. Put it in the <code>save</code> subfolder along-side wherever your ROM file is stored on the SF2000's microSD card (e.g., if your ROM file is <code>sd:/ROMS/Apotris.gba</code>, and you chose save slot 1, you'd put the resulting save state in <code>sd:/ROMS/save/Apotris.gba.sa0</code>).</p><p>The one special case is with arcade games; the ROM you need to select is one of the ones in the <code>sd:/ARCADE/bin/</code> folder, and the resulting save state bundle goes into <code>sd:/ARCADE/save/</code>. As an example, if you wanted to transfer a save state to the SF2000 for the game \"Metal Slug\" in slot 3, you'd select <code>sd:/ARCADE/bin/mslug.zip</code> and Slot 3, and the final location to put the save state would be <code>sd:/ARCADE/save/mslug.zip.sa2</code> - I hope that makes sense! You can <ahref=\"https://vonmillhausen.github.io/sf2000/arcade/DataFrog_SF2000_FBA.html\"target=\"_blank\"rel=\"noreferrernoopener\">find a list of all potential arcade zip-file names and the full name of the game they represent here</a>, if it helps - the first column (\"rom\") is the name of the zip-file without the <code>.zip</code> part, and the second column (\"fullname\") is the full name of the game/romset.</p><divid=\"step3Messages\"></div>";
// Now let's add our controls (file browser for the game ROM, and four
// radio button inputs for the save state slot number); we'll group them
// in one of our control containers for neatness...
html += "<divclass=\"controlContainer\">";
// ROM file browser...
html += "<divclass=\"control\"><label>Select game ROM: <inputid=\"inputROMFile\"type=\"file\"></label></div>";
// Save state slot number...
html += "<divid=\"slotSelector\"class=\"control\"><label><inputtype=\"radio\"name=\"saveSlot\"value=\"0\"> Save Slot 1</label><label><inputtype=\"radio\"name=\"saveSlot\"value=\"1\"> Save Slot 2</label><label><inputtype=\"radio\"name=\"saveSlot\"value=\"2\"> Save Slot 3</label><label><inputtype=\"radio\"name=\"saveSlot\"value=\"3\"> Save Slot 4</label></div>";
html += "</div>";
// ... and our Download button...
html += "<divclass=\"controlContainer\"><divclass=\"control\"><inputid=\"saveStateDownload\"type=\"button\"value=\"Download\"disabled></div></div>";
// ... and lastly we'll close off our section...
html += "</section>";
// Add a <hr> separator after the previous step, and append the new step...
// We're using a cheeky global variable to store whether this event
// listener thinks the chosen file is "good" (i.e., a binary file);
// a separate function then uses this information as part of a check
// to determine if the Download button should be enabled or not. We
// default to "false", and only set to true if the newly chosen file
// appears to be a binary file...
romFileGood = false;
// Now let's read in the file; we don't really care about its contents
// per-se (we only need it for its file name), but we may as well confirm
// it's a binary file while we're here...
var frImage = new FileReader();
saveFileName = event.target.files[0].name;
frImage.readAsDataURL(event.target.files[0]);
frImage.onload = function(event) {
// Check to make sure it's binary...
if (!event.target.result.includes("application/octet-stream")) {
// It's not! Our romFileGood variable is already set to false, so
// run our Download button state-checking function to make sure
// the button is disabled...
userStateDownloadCheck();
// Display a message to the user and return...
document.getElementById("step3Messages").innerHTML = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"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>";
return;
}
else {
// The file appears to be binary, at least! Update our romFileGood
// global to true, and run our Download button state-checking
// function (button may still be disabled if a save state slot
// hasn't been selected)...
romFileGood = true;
userStateDownloadCheck();
}
}
})
// Next up, our radio buttons...
var slots = document.getElementsByName("saveSlot");
for (var i = 0; i <slots.length;i++){
slots[i].addEventListener("change", function() {
// The only thing we'll do here is run our Download button
// 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...
var dButton = document.getElementById("saveStateDownload");
dButton.addEventListener("click", function() {
// We're actually going to be doing quite a bit here - we'll assemble
// the binary blob of the new save state bundle before handing it off
// to our download function. First, let's try zlib-deflating the user's
// save state data...
var compressedSaveState = compressZlib(saveData);
if (!(compressedSaveState instanceof Uint8Array)) {
// 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 = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"aria-hidden=\"true\"src=\""+errorIcon+"\"> ERROR: Failed to compress the chosen save state file's data!</p>";
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 = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"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>";
return false;
}
// Great! We have enough data now to start assembling our final binary
// blob; we'll just re-use our saveData global for convenience...
// 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 = "<pclass=\"error\"><imgclass=\"icon\"alt=\"Aniconofaredcirclewithawhiteexclamationpoint,indicatinganerrorhasoccurred\"aria-hidden=\"true\"src=\""+errorIcon+"\"> ERROR: Inconsistent data length when building save state bundle!</p>";
return false;
}
// Before we kick-off our download function, let's determine the file
// name for the download. The SF2000 uses a slightly odd naming
// convention for its save state files. If the ROM is in the "USER"
// folder on the microSD card, then the extension of the ROM file
// gets capitalised in the save state name (e.g., "Apotris.sfc" would
// become "Apotris.SFC.sa0")... but this *only* happens in the "USER"
// folder from what I can tell. There's no way for me to know what
// folder the user's selected ROM is stored in, so I'm not going to
// bother matching that same convention here (and the SF2000 doesn't
// seem to care about the capitalisation when it comes to loading
// save states)... just thought I'd mention it! So all we're going to
// do here is just stick the appropriate ".sa#" extension on the end