sf2000/tools/buttonMappingChanger.htm
2023-05-10 15:32:01 +01:00

424 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 bisrv.asd 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 bisrv.asd detected</p>";
}
// 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>