mirror of
https://github.com/vonmillhausen/sf2000.git
synced 2024-11-19 08:19:23 +01:00
Added my version of osaka's key mapping tool
This commit is contained in:
parent
260a3ee8d6
commit
f2be096d1c
424
tools/buttonMappingChanger.htm
Normal file
424
tools/buttonMappingChanger.htm
Normal file
@ -0,0 +1,424 @@
|
||||
<!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>
|
Loading…
Reference in New Issue
Block a user