nes-rom-editor / index.html
C50BARZ's picture
please make a dark gray theme with pixel texture and please add more .nes rom editing features - Follow Up Deployment
cbe02bc verified
[ <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NES ROM Editor Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<style>
.hex-cell {
cursor: pointer;
transition: all 0.2s;
}
.hex-cell:hover {
background-color: rgba(59, 130, 246, 0.2);
}
.hex-cell.selected {
background-color: rgba(59, 130, 246, 0.5);
color: white;
}
.ascii-cell {
font-family: monospace;
cursor: pointer;
transition: all 0.2s;
}
.ascii-cell:hover {
background-color: rgba(59, 130, 246, 0.2);
}
.ascii-cell.selected {
background-color: rgba(59, 130, 246, 0.5);
color: white;
}
.hex-editor {
height: 500px;
overflow-y: auto;
font-family: monospace;
}
/* Custom scrollbar */
.hex-editor::-webkit-scrollbar {
width: 8px;
}
.hex-editor::-webkit-scrollbar-track {
background: #f1f1f1;
}
.hex-editor::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.hex-editor::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold text-blue-600 mb-2">
<i class="fas fa-gamepad mr-2"></i> NES ROM Editor
</h1>
<p class="text-gray-600">Edit NES ROM files directly in your browser</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- File Upload Section -->
<div class="bg-white p-6 rounded-lg shadow-md lg:col-span-1">
<h2 class="text-xl font-semibold mb-4 flex items-center">
<i class="fas fa-file-upload mr-2 text-blue-500"></i> ROM File
</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Upload NES ROM</label>
<div class="flex items-center justify-center w-full">
<label class="flex flex-col w-full h-32 border-2 border-dashed hover:border-blue-500 transition-all rounded-lg cursor-pointer">
<div class="flex flex-col items-center justify-center pt-7">
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-500">Drag & drop your ROM file here</p>
<p class="text-xs text-gray-400">or click to browse</p>
</div>
<input type="file" id="romFile" class="opacity-0" accept=".nes" />
</label>
</div>
</div>
<div id="fileInfo" class="hidden mt-4 p-3 bg-blue-50 rounded-lg">
<div class="flex justify-between mb-2">
<span class="font-medium">Filename:</span>
<span id="fileName" class="text-blue-600"></span>
</div>
<div class="flex justify-between mb-2">
<span class="font-medium">Size:</span>
<span id="fileSize" class="text-blue-600"></span>
</div>
<div class="flex justify-between">
<span class="font-medium">ROM Type:</span>
<span id="romType" class="text-blue-600"></span>
</div>
</div>
<div class="mt-6">
<button id="downloadBtn" disabled class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed">
<i class="fas fa-download mr-2"></i> Save Edited ROM
</button>
</div>
<div class="mt-4 border-t pt-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">Quick Actions</h3>
<button id="findTextBtn" disabled class="w-full mb-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
<i class="fas fa-search mr-2"></i> Find Text
</button>
<button id="exportPRGBtn" disabled class="w-full mb-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
<i class="fas fa-code mr-2"></i> Export PRG ROM
</button>
<button id="exportCHRBtn" disabled class="w-full bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
<i class="fas fa-palette mr-2"></i> Export CHR ROM
</button>
</div>
</div>
<!-- Hex Editor Section -->
<div class="bg-white p-6 rounded-lg shadow-md lg:col-span-2">
<h2 class="text-xl font-semibold mb-4 flex items-center">
<i class="fas fa-memory mr-2 text-blue-500"></i> Hex Editor
</h2>
<div class="flex justify-between items-center mb-4">
<div class="flex">
<button id="gotoBtn" disabled class="mr-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-1 px-3 rounded text-sm transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
<i class="fas fa-location-arrow mr-1"></i> Go to...
</button>
<input type="text" id="gotoInput" placeholder="0x8000" disabled class="w-24 px-2 py-1 border rounded text-sm disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
</div>
<div class="text-sm text-gray-500">
<span id="selectionInfo">No selection</span>
</div>
</div>
<div class="hex-editor border rounded-lg p-2 bg-gray-50" id="hexEditorContainer">
<div class="text-center text-gray-400 py-20">
<i class="fas fa-file-alt text-4xl mb-4"></i>
<p>Upload an NES ROM file to begin editing</p>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Selected Value (Hex)</label>
<input type="text" id="hexValueInput" disabled class="w-full px-3 py-2 border rounded bg-gray-100 text-gray-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Selected Value (Dec)</label>
<input type="text" id="decValueInput" disabled class="w-full px-3 py-2 border rounded bg-gray-100 text-gray-400 cursor-not-allowed">
</div>
</div>
</div>
</div>
<!-- Text Editor Section -->
<div class="bg-white p-6 rounded-lg shadow-md mt-6">
<h2 class="text-xl font-semibold mb-4 flex items-center">
<i class="fas fa-font mr-2 text-blue-500"></i> Text Editor
</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">String to Find</label>
<div class="flex">
<input type="text" id="searchText" disabled class="flex-grow px-3 py-2 border rounded-l disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed" placeholder="Enter text to search">
<button id="searchBtn" disabled class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Replacement Text</label>
<div class="flex">
<input type="text" id="replaceText" disabled class="flex-grow px-3 py-2 border rounded-l disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed" placeholder="Enter replacement text">
<button id="replaceBtn" disabled class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed">
<i class="fas fa-exchange-alt"></i>
</button>
</div>
</div>
<div class="border rounded-lg p-3 bg-gray-50 h-40 overflow-auto" id="textPreview">
<div class="text-center text-gray-400 py-10">
<i class="fas fa-file-alt text-2xl mb-2"></i>
<p>No ROM loaded to display text</p>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="bg-gray-800 text-white p-2 rounded-b-lg text-sm mt-6 flex justify-between items-center">
<div class="flex items-center">
<i class="fas fa-info-circle mr-2 text-blue-300"></i>
<span id="statusMessage">Ready to upload NES ROM file</span>
</div>
<div id="loadingSpinner" class="hidden">
<i class="fas fa-spinner fa-spin mr-2"></i>
<span>Processing...</span>
</div>
</div>
</div>
<script>
// Global variables
let romData = null;
let selectedOffset = null;
let searchResults = [];
// DOM elements
const romFileInput = document.getElementById('romFile');
const downloadBtn = document.getElementById('downloadBtn');
const hexEditorContainer = document.getElementById('hexEditorContainer');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const romType = document.getElementById('romType');
const gotoBtn = document.getElementById('gotoBtn');
const gotoInput = document.getElementById('gotoInput');
const selectionInfo = document.getElementById('selectionInfo');
const hexValueInput = document.getElementById('hexValueInput');
const decValueInput = document.getElementById('decValueInput');
const searchText = document.getElementById('searchText');
const searchBtn = document.getElementById('searchBtn');
const replaceText = document.getElementById('replaceText');
const replaceBtn = document.getElementById('replaceBtn');
const textPreview = document.getElementById('textPreview');
const statusMessage = document.getElementById('statusMessage');
const loadingSpinner = document.getElementById('loadingSpinner');
const findTextBtn = document.getElementById('findTextBtn');
const exportPRGBtn = document.getElementById('exportPRGBtn');
const exportCHRBtn = document.getElementById('exportCHRBtn');
// Helper functions
function formatHex(value, length) {
return value.toString(16).toUpperCase().padStart(length, '0');
}
function parseHex(hexString) {
return parseInt(hexString, 16);
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' bytes';
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
else return (bytes / 1048576).toFixed(2) + ' MB';
}
function updateStatus(message) {
statusMessage.textContent = message;
}
function showLoading(show) {
if (show) {
loadingSpinner.classList.remove('hidden');
} else {
loadingSpinner.classList.add('hidden');
}
}
function enableControls(enable) {
const controls = [
downloadBtn, gotoBtn, gotoInput, searchText,
searchBtn, replaceText, replaceBtn, findTextBtn,
exportPRGBtn, exportCHRBtn
];
controls.forEach(control => {
control.disabled = !enable;
});
if (enable) {
hexValueInput.disabled = false;
decValueInput.disabled = false;
hexValueInput.classList.remove('bg-gray-100', 'text-gray-400', 'cursor-not-allowed');
decValueInput.classList.remove('bg-gray-100', 'text-gray-400', 'cursor-not-allowed');
} else {
hexValueInput.disabled = true;
decValueInput.disabled = true;
hexValueInput.classList.add('bg-gray-100', 'text-gray-400', 'cursor-not-allowed');
decValueInput.classList.add('bg-gray-100', 'text-gray-400', 'cursor-not-allowed');
}
}
function renderHexEditor(data) {
if (!data) return;
showLoading(true);
updateStatus("Rendering hex editor...");
// Start rendering in chunks to avoid freezing the UI
setTimeout(() => {
const chunkSize = 4096; // bytes per chunk
let offset = 0;
hexEditorContainer.innerHTML = '';
// Create header
const header = document.createElement('div');
header.className = 'flex text-sm font-mono mb-1 sticky top-0 bg-gray-100 z-10';
// Address header
const addrHeader = document.createElement('div');
addrHeader.className = 'w-20 font-semibold text-gray-600';
addrHeader.textContent = 'Offset';
header.appendChild(addrHeader);
// Hex values header
for (let i = 0; i < 16; i++) {
const hexHeader = document.createElement('div');
hexHeader.className = 'w-6 text-center font-semibold text-gray-600';
hexHeader.textContent = formatHex(i, 2);
header.appendChild(hexHeader);
}
// ASCII header
const asciiHeader = document.createElement('div');
asciiHeader.className = 'w-64 ml-4 font-semibold text-gray-600';
asciiHeader.textContent = 'ASCII';
header.appendChild(asciiHeader);
hexEditorContainer.appendChild(header);
// Render chunks
function renderChunk() {
const fragment = document.createDocumentFragment();
const end = Math.min(offset + chunkSize, data.byteLength);
for (; offset < end; offset += 16) {
const row = document.createElement('div');
row.className = 'flex text-sm font-mono hover:bg-gray-50';
// Address
const addrCell = document.createElement('div');
addrCell.className = 'w-20 text-gray-500';
addrCell.textContent = formatHex(offset, 6);
row.appendChild(addrCell);
// Hex values
const hexRow = document.createElement('div');
hexRow.className = 'flex';
for (let i = 0; i < 16; i++) {
const pos = offset + i;
if (pos >= data.byteLength) break;
const value = data[pos];
const hexCell = document.createElement('div');
hexCell.className = 'hex-cell w-6 text-center';
hexCell.textContent = formatHex(value, 2);
hexCell.dataset.offset = pos;
hexCell.addEventListener('click', () => {
selectCell(pos);
});
hexRow.appendChild(hexCell);
}
row.appendChild(hexRow);
// ASCII representation
const asciiRow = document.createElement('div');
asciiRow.className = 'flex ml-4';
for (let i = 0; i < 16; i++) {
const pos = offset + i;
if (pos >= data.byteLength) break;
const value = data[pos];
let char = value >= 32 && value <= 126 ? String.fromCharCode(value) : '.';
const asciiCell = document.createElement('div');
asciiCell.className = 'ascii-cell w-4 text-center';
asciiCell.textContent = char;
asciiCell.dataset.offset = pos;
asciiCell.addEventListener('click', () => {
selectCell(pos);
});
asciiRow.appendChild(asciiCell);
}
row.appendChild(asciiRow);
fragment.appendChild(row);
}
hexEditorContainer.appendChild(fragment);
if (offset < data.byteLength) {
// Continue rendering in next animation frame
requestAnimationFrame(renderChunk);
} else {
showLoading(false);
updateStatus("ROM loaded successfully");
// Analyze ROM header
analyzeRomHeader(data);
// Update text preview
updateTextPreview(data);
}
}
requestAnimationFrame(renderChunk);
}, 100);
}
function selectCell(offset) {
// Clear previous selection
const prevSelected = document.querySelectorAll('.hex-cell.selected, .ascii-cell.selected');
prevSelected.forEach(el => el.classList.remove('selected'));
// Find and highlight new selection
const hexCells = document.querySelectorAll(`.hex-cell[data-offset="${offset}"]`);
const asciiCells = document.querySelectorAll(`.ascii-cell[data-offset="${offset}"]`);
hexCells.forEach(el => el.classList.add('selected'));
asciiCells.forEach(el => el.classList.add('selected'));
// Scroll into view
if (hexCells.length > 0) {
hexCells[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Update selection info
selectedOffset = offset;
selectionInfo.textContent = `Selected: 0x${formatHex(offset, 6)}`;
// Update value inputs
const value = romData[offset];
hexValueInput.value = formatHex(value, 2);
decValueInput.value = value.toString();
// Enable editing
hexValueInput.addEventListener('change', handleHexValueChange);
decValueInput.addEventListener('change', handleDecValueChange);
}
function handleHexValueChange(e) {
const hexValue = e.target.value.trim();
if (!hexValue) return;
const intValue = parseInt(hexValue, 16);
if (isNaN(intValue) || intValue < 0 || intValue > 255) {
alert('Please enter a valid hex value (00-FF)');
e.target.value = formatHex(romData[selectedOffset], 2);
return;
}
// Update ROM data
romData[selectedOffset] = intValue;
// Update dec value
decValueInput.value = intValue.toString();
// Update UI
const hexCells = document.querySelectorAll(`.hex-cell[data-offset="${selectedOffset}"]`);
const asciiCells = document.querySelectorAll(`.ascii-cell[data-offset="${selectedOffset}"]`);
hexCells.forEach(el => el.textContent = formatHex(intValue, 2));
asciiCells.forEach(el => el.textContent = intValue >= 32 && intValue <= 126 ? String.fromCharCode(intValue) : '.');
updateStatus(`Changed byte at 0x${formatHex(selectedOffset, 6)} to 0x${formatHex(intValue, 2)}`);
}
function handleDecValueChange(e) {
const decValue = e.target.value.trim();
if (!decValue) return;
const intValue = parseInt(decValue, 10);
if (isNaN(intValue) || intValue < 0 || intValue > 255) {
alert('Please enter a valid decimal value (0-255)');
e.target.value = romData[selectedOffset].toString();
return;
}
// Update ROM data
romData[selectedOffset] = intValue;
// Update hex value
hexValueInput.value = formatHex(intValue, 2);
// Update UI
const hexCells = document.querySelectorAll(`.hex-cell[data-offset="${selectedOffset}"]`);
const asciiCells = document.querySelectorAll(`.ascii-cell[data-offset="${selectedOffset}"]`);
hexCells.forEach(el => el.textContent = formatHex(intValue, 2));
asciiCells.forEach(el => el.textContent = intValue >= 32 && intValue <= 126 ? String.fromCharCode(intValue) : '.');
updateStatus(`Changed byte at 0x${formatHex(selectedOffset, 6)} to ${intValue}`);
}
function analyzeRomHeader(data) {
if (data.byteLength < 16) return;
// Check NES header
if (data[0] !== 0x4E || data[1] !== 0x45 || data[2] !== 0x53 || data[3] !== 0x1A) {
updateStatus("Warning: File may not be a valid NES ROM (missing header magic)", 'warning');
return;
}
// Get ROM info
const prgSize = data[4] * 16; // 16KB units
const chrSize = data[5] * 8; // 8KB units
const flags6 = data[6];
const flags7 = data[7];
// Determine mapper number
const mapper = (flags6 >> 4) | (flags7 & 0xF0);
// Determine mirroring type
const mirroring = (flags6 & 0x1) ? 'Vertical' : 'Horizontal';
const fourScreen = (flags6 & 0x8) ? ' (Four-screen)' : '';
// Determine console type
const nes2Format = (flags7 & 0x0C) === 0x08;
const consoleType = nes2Format ? 'NES 2.0' : 'iNES';
// Update info display
fileInfo.classList.remove('hidden');
romType.textContent = `${consoleType} ROM - Mapper ${mapper}`;
updateStatus(`Analyzed ROM: ${prgSize}KB PRG, ${chrSize}KB CHR, ${mirroring}${fourScreen} mirroring`);
}
function updateTextPreview(data) {
if (!data) return;
// Extract text from ROM (simple ASCII)
let text = '';
let inString = false;
for (let i = 16; i < data.byteLength; i++) { // Skip header
const byte = data[i];
if (byte >= 32 && byte <= 126) {
if (!inString) {
text += '\n'; // New line for new string
inString = true;
}
text += String.fromCharCode(byte);
} else {
inString = false;
}
}
// Display text
textPreview.innerHTML = '';
const pre = document.createElement('pre');
pre.className = 'whitespace-pre-wrap font-mono text-sm';
pre.textContent = text.trim();
textPreview.appendChild(pre);
}
function searchTextInRom() {
const searchString = searchText.value.trim();
if (!searchString || !romData) return;
showLoading(true);
updateStatus(`Searching for "${searchString}"...`);
searchResults = [];
const searchBytes = [];
// Convert search string to bytes
for (let i = 0; i < searchString.length; i++) {
searchBytes.push(searchString.charCodeAt(i));
}
// Search through ROM data
for (let i = 0; i <= romData.byteLength - searchBytes.length; i++) {
let match = true;
for (let j = 0; j < searchBytes.length; j++) {
if (romData[i + j] !== searchBytes[j]) {
match = false;
break;
}
}
if (match) {
searchResults.push(i);
}
}
showLoading(false);
if (searchResults.length > 0) {
// Highlight first match
selectCell(searchResults[0]);
updateStatus(`Found ${searchResults.length} occurrence(s) of "${searchString}"`);
} else {
updateStatus(`Text "${searchString}" not found in ROM`);
}
}
function replaceTextInRom() {
const searchStr = searchText.value.trim();
const replaceStr = replaceText.value.trim();
if (!searchStr || !replaceStr || searchResults.length === 0 || !romData) return;
if (searchStr.length !== replaceStr.length) {
alert('Search and replace strings must be the same length');
return;
}
showLoading(true);
updateStatus(`Replacing ${searchResults.length} occurrence(s)...`);
const replaceBytes = [];
// Convert replace string to bytes
for (let i = 0; i < replaceStr.length; i++) {
replaceBytes.push(replaceStr.charCodeAt(i));
}
// Perform replacements
searchResults.forEach(offset => {
for (let i = 0; i < replaceBytes.length; i++) {
romData[offset + i] = replaceBytes[i];
}
});
// Update UI
setTimeout(() => {
renderHexEditor(romData);
updateTextPreview(romData);
showLoading(false);
updateStatus(`Replaced ${searchResults.length} occurrence(s) of "${searchStr}" with "${replaceStr}"`);
}, 100);
}
function exportPRG() {
if (!romData || romData.byteLength < 16) return;
const prgSize = romData[4] * 16384; // 16KB pages
if (prgSize <= 0) return;
const prgStart = 16; // Skip header
const prgEnd = prgStart + prgSize;
const prgData = new Uint8Array(romData.slice(prgStart, prgEnd));
downloadData(prgData, 'prg_rom.bin');
updateStatus(`Exported ${formatFileSize(prgSize)} PRG ROM`);
}
function exportCHR() {
if (!romData || romData.byteLength < 16) return;
const prgSize = romData[4] * 16384; // 16KB pages
const chrSize = romData[5] * 8192; // 8KB pages
if (chrSize <= 0) return;
const chrStart = 16 + prgSize; // Skip header and PRG ROM
const chrEnd = chrStart + chrSize;
const chrData = new Uint8Array(romData.slice(chrStart, chrEnd));
downloadData(chrData, 'chr_rom.bin');
updateStatus(`Exported ${formatFileSize(chrSize)} CHR ROM`);
}
function downloadData(data, filename) {
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
// Event listeners
romFileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
showLoading(true);
updateStatus(`Loading ${file.name}...`);
const reader = new FileReader();
reader.onload = function(e) {
try {
const arrayBuffer = e.target.result;
romData = new Uint8Array(arrayBuffer);
// Update file info
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
// Render editor
renderHexEditor(romData);
enableControls(true);
} catch (err) {
console.error(err);
updateStatus('Error loading ROM file', 'error');
showLoading(false);
}
};
reader.onerror = function() {
updateStatus('Error reading file', 'error');
showLoading(false);
};
reader.readAsArrayBuffer(file);
});
downloadBtn.addEventListener('click', function() {
if (!romData) return;
showLoading(true);
updateStatus("Preparing download...");
setTimeout(() => {
// Get filename from input or use default
let filename = fileName.textContent || 'edited_rom.nes';
// Ensure .nes extension
if (!filename.toLowerCase().endsWith('.nes')) {
filename += '.nes';
}
downloadData(romData, filename);
showLoading(false);
updateStatus(`Downloaded edited ROM as ${filename}`);
}, 100);
});
searchBtn.addEventListener('click', searchTextInRom);
replaceBtn.addEventListener('click', replaceTextInRom);
findTextBtn.addEventListener('click', function() {
searchText.focus();
});
exportPRGBtn.addEventListener('click', exportPRG);
exportCHRBtn.addEventListener('click', exportCHR);
gotoBtn.addEventListener('click', function() {
const offsetStr = gotoInput.value.trim();
if (!offsetStr || !romData) return;
let offset;
if (offsetStr.startsWith('0x')) {
offset = parseHex(offsetStr.substring(2));
} else {
offset = parseInt(offsetStr, 10);
}
if (isNaN(offset) || offset < 0 || offset >= romData.byteLength) {
alert('Invalid offset');
return;
}
selectCell(offset);
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://deepsite.hf.co/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://deepsite.hf.co" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://deepsite.hf.co?remix=C50BARZ/nes-rom-editor" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>