Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| [ | |
| <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> | 
