mirror of
https://github.com/vonmillhausen/sf2000.git
synced 2025-02-01 19:42:38 +01:00
db0f33debe
Seems like there may be some more significant changes to the data format for the button mappings; I haven't had time to dive into the details yet, so just updating the tool to indicate it knows about the new firmware, but doesn't support it yet
431 lines
23 KiB
HTML
431 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
<title>Data Frog SF2000 Button Mapping Tool</title>
|
|
<meta name="viewport" content="width=device-width">
|
|
<style>
|
|
:root {
|
|
--background: rgb(240, 235, 220);
|
|
--text: rgb(50, 40, 20);
|
|
--errorBackground: rgb(200, 65, 65);
|
|
--errorText: rgb(255, 255, 255);
|
|
--infoBackground: rgb(65, 160, 65);
|
|
--infoText: rgb(255, 255, 255);
|
|
--mappingBox: rgba(50, 40, 20, 0.1);
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--background: rgb(70, 75, 100);
|
|
--text: rgb(190, 190, 200);
|
|
--errorBackground: rgb(130, 85, 75);
|
|
--errorText: rgb(245, 200, 200);
|
|
--infoBackground: rgb(75, 130, 85);
|
|
--infoText: rgb(200, 245, 200);
|
|
--mappingBox: rgba(190, 190, 200, 0.1);
|
|
}
|
|
}
|
|
body {
|
|
background-color: var(--background);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
|
}
|
|
a, a:visited, a:hover, a:active { color: var(--text); }
|
|
hr {
|
|
border: 1px solid var(--text);
|
|
margin: 2em 0;
|
|
}
|
|
p.errorMessage, p.infoMessage {
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
margin: 20px;
|
|
}
|
|
p.errorMessage {
|
|
background-color: var(--errorBackground);
|
|
border: 1px dashed var(--errorText);
|
|
color: var(--errorText);
|
|
}
|
|
p.infoMessage {
|
|
background-color: var(--infoBackground);
|
|
border: 1px dashed var(--infoText);
|
|
color: var(--infoText);
|
|
}
|
|
h1:first-child { text-align: center; }
|
|
p:last-child { text-align: center; }
|
|
#mappingControls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
/*display: grid;*/
|
|
/*grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));*/
|
|
/*grid-row-gap: 1em;*/
|
|
/*grid-column-gap: 1em;*/
|
|
}
|
|
.mappingConsole {
|
|
background-color: var(--mappingBox);
|
|
padding: 1em;
|
|
border-radius: 1em;
|
|
max-width: 18em;
|
|
margin: 0.5em;
|
|
}
|
|
.mappingConsole h3:first-child {
|
|
text-align: center;
|
|
margin-top: 0;
|
|
border-bottom: 1px solid var(--text);
|
|
}
|
|
.mappingConsole table {
|
|
width: 100%;
|
|
border-spacing: 0 0.1em;
|
|
background-color: var(--mappingBox);
|
|
border-radius: 0.5em;
|
|
padding: 0.5em;
|
|
}
|
|
.mappingConsole table:nth-child(2) { margin-bottom: 1em; }
|
|
.mappingConsole table caption { margin-bottom: 0.5em; }
|
|
.mappingConsole table thead tr th {
|
|
border-bottom: 1px solid var(--text);
|
|
border-collapse: separate;
|
|
border-spacing: 1em 1em;
|
|
}
|
|
.alignC { text-align: center; }
|
|
.alignL { text-align: left; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Data Frog SF2000 Button Mapping Tool</h1>
|
|
<p>This tool lets you alter the button mappings for the SF2000 hand-held console; it can generate per-game mappings, as well as alter the global mappings defined in the device's <code>bisrv.asd</code> BIOS file. As the SF2000 supports multiplayer gaming via an optional wireless controller (sold separately), mappings for both Player 1 and Player 2 are possible. Please note this tool is provided as-is, and no support will be given if this corrupts your device's BIOS; make sure you have backups of anything you care about before messing with your device's critical files! 🙂</p>
|
|
<p>This tool was originally written by nikita.burnashev (email) gmail.com; it was re-written (mostly just re-styled) by myself upon their request.</p>
|
|
<hr>
|
|
<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. If you're choosing your <code>bisrv.asd</code> file, you should probably make a backup of it first, just in case! 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 <code>bisrv.asd</code> or game ROM: <input id="fileSelector" type="file" onchange="fileLoad(event.target.files[0])"></label>
|
|
</form>
|
|
<div id="fileOutput"></div>
|
|
</section>
|
|
<hr>
|
|
<section id="mappingSection">
|
|
<h2>Step 2: Choose your button mappings</h2>
|
|
<p id="mappingInstructions">Instructions for Step 2 will appear here when you have chosen a file in Step 1 above.</p>
|
|
<div id="mappingControls"></div>
|
|
</section>
|
|
<hr>
|
|
<section id="saveSection">
|
|
<h2>Step 3: Save your mapping changes</h2>
|
|
<p id="saveInstructions">Instructions for Step 3 will appear here when you have chosen a file in Step 1 above.</p>
|
|
<div id="saveControls"></div>
|
|
</section>
|
|
<script>
|
|
|
|
// Global variables...
|
|
var mappingTableOffset; // Will contain the offset of the button mappings within the bisrv.asd file
|
|
var mappingConsoles; // Will contain a list of the specific game consoles we'll be setting up mappings for
|
|
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
|
|
|
|
// Utility function: getButtonMap(int index)
|
|
// =========================================
|
|
// This function returns data about how given buttons on the SF2000
|
|
// map to controls provided by the device's emulators.
|
|
// Thanks to @notv37 :)
|
|
// https://discord.com/channels/741895796315914271/1099465777825972347/1104285497804738640
|
|
function getButtonMap(index) {
|
|
if (mappingConsoles[index] == "Genesis/Mega Drive, Master System")
|
|
return { 'A': 8, 'B': 0, 'C': 1, 'X': 10, 'Y': 11, 'Z': 9 };
|
|
else if (mappingConsoles[index] == "Arcade") // FIXME
|
|
return { 'A': 8, 'B': 0, 'C': 1, 'X': 10, 'Y': 11, 'Z': 9 };
|
|
else if (mappingConsoles[index] == "SNES")
|
|
return { 'A': 8, 'B': 0, 'X': 10, 'Y': 11, 'L': 9, 'R': 1 };
|
|
else // GBA, GB/GBC, NES
|
|
return { 'A': 8, 'B': 0, 'L': 10, 'R': 11, 'X': 9, 'Y': 1 };
|
|
}
|
|
|
|
// This function is called whenever a file is selected in Step 1...
|
|
function fileLoad(file) {
|
|
|
|
// Create a FileReader object, and read in the selected file's contents as
|
|
// 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;
|
|
|
|
// Reset our Step 2 and Step 3 instructions and controls...
|
|
document.getElementById("mappingInstructions").innerHTML = "Instructions for Step 2 will appear here when you have chosen a file in Step 1 above.";
|
|
document.getElementById("mappingControls").innerHTML = "";
|
|
document.getElementById("saveInstructions").innerHTML = "Instructions for Step 3 will appear here when you have chosen a file in Step 1 above.";
|
|
document.getElementById("saveControls").innerHTML = "";
|
|
|
|
// Read the provided file's data from the buffer array into an unsigned 8-bit int array...
|
|
var data = new Uint8Array(event.target.result);
|
|
|
|
// Let's check the data to see what kind of file we got. First, let's
|
|
// check if it looks like one of the known bisrv.asd versions...
|
|
if (data.length == 12647452) {
|
|
// That's the correct length for an original March 28th version of bisrv.asd, so
|
|
// set up for re-mapping all game consoles...
|
|
mappingTableOffset = 0x8DBC0C;
|
|
mappingConsoles = ["Arcade", "Game Boy Advance", "SNES", "Genesis/Mega Drive, Master System", "NES, Game Boy, Game Boy Color"];
|
|
bisrvData = data;
|
|
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: March 28th <code>bisrv.asd</code> detected</p>";
|
|
}
|
|
else if (data.length == 12648068) {
|
|
// That's the correct length for an April 20th version of bisrv.asd, so
|
|
// set up for re-mapping all game consoles...
|
|
mappingTableOffset = 0x8DBC9C;
|
|
mappingConsoles = ["Arcade", "Game Boy Advance", "Game Boy, Game Boy Color", "SNES", "Genesis/Mega Drive, Master System", "NES"];
|
|
bisrvData = data;
|
|
document.getElementById("fileOutput").innerHTML = "<p class=\"infoMessage\">INFO: April 20th <code>bisrv.asd</code> detected</p>";
|
|
}
|
|
else if (data.length == 12656068) {
|
|
// That's the correct length for the May 15th version of the file
|
|
// However this firmware version is not currently supported by this tool
|
|
// Report this to the user to help avoid confusion in the meantime...
|
|
document.getElementById("fileOutput").innerHTML = "<p class=\"errorMessage\">ERROR: May 15th <code>bisrv.asd</code> detected, however this version of the firmware is not currently supported. Please use the SF2000's new built-in button mapping feature in the meantime.</p>";
|
|
return;
|
|
}
|
|
// If we're still checking, next test the file extensions for the individual console's ROMs...
|
|
else if (/\.(zfb|zip)$/i.exec(file.name)) {
|
|
// The file's name ends with .zfb or .zip - assume it's an arcade ROM!
|
|
mappingConsoles = ["Arcade"];
|
|
}
|
|
else if (/\.(zgb|gba|agb|gbz)$/i.exec(file.name)) {
|
|
// The file's name ends with .zgb, .gba, .agb or .gbz - assume it's a Game Boy Advance ROM!
|
|
mappingConsoles = ["Game Boy Advance"];
|
|
}
|
|
else if (/\.(gbc|gb|sgb)$/i.exec(file.name)) {
|
|
// The file's name ends with .gbc, .gb or .sgb - assume it's a Game Boy or Game Boy Color ROM!
|
|
mappingConsoles = ["Game Boy, Game Boy Color"];
|
|
}
|
|
else if (/\.(zsf|smc|fig|sfc|gd3|gd7|dx2|bsx|swc)$/i.exec(file.name)) {
|
|
// The file's name ends with .zsf, .smc, .fig, .sfc, .gd3, .gd7, .dx2, .bsx or .swc - assume it's a SNES ROM!
|
|
mappingConsoles = ["SNES"];
|
|
}
|
|
else if (/\.(zmd|bin|md|smd|gen|sms)$/i.exec(file.name)) {
|
|
// The file's name ends with .zmd, .bin, .md, .smd, .gen or .sms - assume it's a Genesis/Mega Drive or Master System ROM!
|
|
mappingConsoles = ["Genesis/Mega Drive, Master System"];
|
|
}
|
|
else if (/\.(zfc|nes|nfc|fds|unf)$/i.exec(file.name)) {
|
|
// The file's name ends with .zfc, .nes, .nfc, .fds or .unf - assume it's a NES ROM!
|
|
mappingConsoles = ["NES"];
|
|
}
|
|
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 bisrv.asd file, or a game ROM with a known extension!</p>";
|
|
return;
|
|
}
|
|
|
|
// If we're here, then we got some kind of file we're happy with. If mappingConsoles
|
|
// only contains one entry, then it was a ROM file, and we'll want to initialise our
|
|
// mappingData array with 48 slots; otherwise, it was a bisrv.asd and we'll set
|
|
// mappingData to it's full contents instead...
|
|
if (mappingConsoles.length == 1) {
|
|
mappingData = new Uint8Array(48);
|
|
mappingTableOffset = 0;
|
|
fileName = file.name;
|
|
}
|
|
else {
|
|
mappingData = data;
|
|
}
|
|
|
|
// Go ahead call our Step Two function...
|
|
stepTwo();
|
|
}
|
|
}
|
|
|
|
function stepTwo() {
|
|
// We're going to be creating a bunch of HTML here; we want to display banks of mapping
|
|
// controls to the user, one bank per console. Each bank will have a heading specifying
|
|
// 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.
|
|
|
|
// First, we need to update Step 2's instructions, depending on whether or not the user
|
|
// supplied a bisrv.asd file (multiple consoles) or a ROM (one console)...
|
|
if (mappingConsoles.length > 1) {
|
|
// They provided a bisrv.asd file!
|
|
document.getElementById("mappingInstructions").innerHTML = "Below you will see the current global button mappings for the <code>bisrv.asd</code> bios 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.";
|
|
}
|
|
else {
|
|
// They provided a ROM file!
|
|
document.getElementById("mappingInstructions").innerHTML = "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.";
|
|
}
|
|
|
|
// Next we'll be looping through all of the consoles we'll be setting up mappings for...
|
|
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...
|
|
var currentConsoleNode = document.createElement("div");
|
|
currentConsoleNode.className = "mappingConsole";
|
|
currentConsoleNode.innerHTML += "<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...
|
|
for (var player = 0; player < 2; player++) {
|
|
|
|
// Start creating our table HTML...
|
|
var tNode = "<table><caption>Player " + (player + 1) + "</caption>";
|
|
tNode += "<thead><tr><th class=\"alignL\">SF2000</th><th>Console</th><th>Autofire</th></tr></thead>";
|
|
tNode += "<tbody>";
|
|
|
|
// Loop through all the SF2000's buttons (well, the ones that can be mapped, anyway)...
|
|
for (var button = 0; button < 6; button++) {
|
|
|
|
// Calculate our offset within our mapping data for the current button...
|
|
var offset = mappingTableOffset + (currentConsole * 48) + (player * 24) + (button * 4);
|
|
|
|
// Start creating the HTML data for this row in the table...
|
|
var tRowHTML = "<tr>";
|
|
|
|
// SF2000 Button Name (e.g., "Player 1 X")...
|
|
tRowHTML += "<td>Player " + (player + 1).toString() + " " + ['X', 'Y' ,'L', 'A', 'B', 'R'][button] + "</td>";
|
|
|
|
// Console button selection list...
|
|
tRowHTML += "<td class=\"alignC\">";
|
|
tRowHTML += "<select id=\"sel" + offset.toString(16) + "\">";
|
|
for (var buttonTable in buttonMap) {
|
|
tRowHTML += "<option ";
|
|
if (mappingData[offset] == buttonMap[buttonTable]) {
|
|
tRowHTML += "selected";
|
|
}
|
|
tRowHTML += ">" + buttonTable + "</option>";
|
|
}
|
|
tRowHTML += "</select></td>";
|
|
|
|
// Autofire checkbox...
|
|
tRowHTML += "<td class=\"alignC\"><input id=\"cb" + offset.toString(16) + "\" type=\"checkbox\"";
|
|
if (mappingData[offset + 2] == 1) {
|
|
tRowHTML += " checked";
|
|
}
|
|
tRowHTML += "></td>";
|
|
|
|
// And we're finished with the row...
|
|
tRowHTML += "</tr>";
|
|
tNode += tRowHTML;
|
|
}
|
|
|
|
// Close off our table body, and add it to the console's <div>...
|
|
tNode += "</tbody>";
|
|
currentConsoleNode.innerHTML += tNode;
|
|
}
|
|
|
|
// Finally, add this console's <div> to our mappingControls container...
|
|
document.getElementById("mappingControls").appendChild(currentConsoleNode);
|
|
}
|
|
|
|
// OK, we're all done displaying our mapping table HTML; trigger Step 3's setup...
|
|
stepThreeSetup();
|
|
}
|
|
|
|
function stepThreeSetup() {
|
|
// More HTML in this function! We'll display the appropriate instructions to the
|
|
// user (either how to replace the bisrv.asd file, or where to put the .kmp file),
|
|
// as well as generate a button that (when clicked) will download the appropriate
|
|
// file to their device...
|
|
|
|
// First up, instructions! These will depend on whether they provided a bisrv.asd
|
|
// or a game ROM...
|
|
if (mappingConsoles.length > 1) {
|
|
// They provided a bisrv.asd file!
|
|
document.getElementById("saveInstructions").innerHTML = "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.";
|
|
}
|
|
else {
|
|
// 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...
|
|
document.getElementById("saveInstructions").innerHTML = "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.";
|
|
}
|
|
|
|
// Now let's add the Download button with it's event...
|
|
document.getElementById("saveControls").innerHTML = "<form id=\"downloadForm\" action=\"#\"><input id=\"downloadButton\" type=\"button\" value=\"Download\" onclick=\"download()\"></form>";
|
|
}
|
|
|
|
function download() {
|
|
// Here, we'll construct the file for the user to download (either a modified
|
|
// bisrv.asd, or a .kmp keymap file), and send it to the user's browser...
|
|
|
|
// We need to loop through all of the mapping form data, read its settings, and
|
|
// use those settings to build the binary data of our button mapping. Loop
|
|
// through all of the consoles we're mapping for...
|
|
for (var currentConsole = 0; currentConsole < mappingConsoles.length; currentConsole ++) {
|
|
|
|
// 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++) {
|
|
// 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) + (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) {
|
|
// 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;
|
|
}
|
|
c = ~0;
|
|
for (var i = 512; i < mappingData.length; i++) {
|
|
c = c << 8 ^ tabCRC32[c >>> 24 ^ mappingData[i]];
|
|
}
|
|
mappingData[0x18c] = c & 255;
|
|
mappingData[0x18d] = c >>> 8 & 255;
|
|
mappingData[0x18e] = c >>> 16 & 255;
|
|
mappingData[0x18f] = c >>> 24;
|
|
}
|
|
|
|
// Download time! First, let's determine the name of the file we're sending
|
|
// to the user's browser...
|
|
var downloadFileName = "bisrv.asd";
|
|
if (mappingConsoles.length == 1) {
|
|
downloadFileName = fileName.replace(/($|\.[^.]*$)/, function(m, p1) {return p1.toUpperCase() + '.kmp';});
|
|
}
|
|
|
|
// 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="http://creativecommons.org/publicdomain/zero/1.0/">CC0</a>: public domain. Version 1.0, 20230510.1</p>
|
|
</body>
|
|
</html> |