-
- © 2024 SCUMM NES resource explorer
-
-
- {navigation.map((item) => (
-
- {item.name}
-
-
- ))}
- {process.env.NODE_ENV === 'development' && (
-
- XS
- SM
- MD
- LG
- XL
- 2XL
-
- )}
-
+const Footer = () => (
+
+);
+
+export default Footer;
diff --git a/src/components/Header.js b/src/components/Header.js
index 6b25eac..59eb6e1 100644
--- a/src/components/Header.js
+++ b/src/components/Header.js
@@ -1,5 +1,6 @@
import { Fragment, useState } from 'react';
import { Link } from 'react-router-dom';
+import DownloadRom from './DownloadRom';
import {
Dialog,
DialogPanel,
@@ -7,6 +8,7 @@ import {
TransitionChild,
} from '@headlessui/react';
import {
+ ArrowDownTrayIcon,
Cog8ToothIcon,
Bars3Icon,
XMarkIcon,
@@ -22,7 +24,7 @@ const navigation = [
{ name: 'Settings', href: '/settings', sideBarOnly: true },
];
-export default function Header() {
+const Header = () => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
@@ -63,7 +65,13 @@ export default function Header() {
))}
-
-
-
- {children}
-
-
+const Layout = ({ children }) => (
+
+
+
+ {children}
- );
-}
+
+
+);
+
+export default Layout;
diff --git a/src/containers/DropZoneContainer.js b/src/containers/DropZoneContainer.js
index 1d7125a..0c55279 100644
--- a/src/containers/DropZoneContainer.js
+++ b/src/containers/DropZoneContainer.js
@@ -1,13 +1,16 @@
import { useState, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
+import { useNavigate } from 'react-router-dom';
+import { useRomDispatch } from '../contexts/RomContext';
+import parseRom from '../lib/parser/parseRom';
import Main from '../components/Main';
import DropZone from '../components/DropZone';
import BaseRomDialog from '../components/BaseRomDialog';
-const DropZoneContainer = ({ onFile }) => {
+const DropZoneContainer = () => {
const validator = (file) => {
setErrorCode(null);
- setRom(null);
+ setPrg(null);
setRes(null);
if (!file.name) {
@@ -28,7 +31,7 @@ const DropZoneContainer = ({ onFile }) => {
setErrorCode('reading-file-failed');
};
reader.onload = async () => {
- const { hasNesHeader } = await import('../lib/utils');
+ const { hasNesHeader } = await import('../lib/romUtils');
const { default: crc32 } = await import('../lib/crc32');
const { isJapaneseVersion, getResFromCrc32 } = await import(
'../lib/getResFromCrc32'
@@ -50,7 +53,7 @@ const DropZoneContainer = ({ onFile }) => {
const res = getResFromCrc32(c);
- setRom(arrayBuffer);
+ setPrg(arrayBuffer);
if (!res) {
setBaseRomDialogOpened(true);
@@ -64,10 +67,11 @@ const DropZoneContainer = ({ onFile }) => {
return null;
};
+ const dispatch = useRomDispatch();
const [errorCode, setErrorCode] = useState(null);
const [baseRomDialogOpened, setBaseRomDialogOpened] = useState(false);
const [baseRom, setBaseRom] = useState(null);
- const [rom, setRom] = useState(null);
+ const [prg, setPrg] = useState(null);
const [res, setRes] = useState(null);
const {
getRootProps,
@@ -81,15 +85,15 @@ const DropZoneContainer = ({ onFile }) => {
multiple: false,
validator,
});
+ const navigate = useNavigate();
if (baseRom) {
(async () => {
const { getResFromBaseRom } = await import('../lib/getResFromCrc32');
- const { default: parseRom } = await import('../lib/parser/parseRom');
const res = getResFromBaseRom(baseRom);
try {
- parseRom(rom, res);
+ parseRom(prg, res);
setRes(res);
} catch (err) {
setErrorCode('invalid-rom-file');
@@ -102,10 +106,17 @@ const DropZoneContainer = ({ onFile }) => {
}
useEffect(() => {
- if (acceptedFiles.length === 1 && rom && res) {
- onFile(rom, res);
+ if (acceptedFiles.length === 1 && prg && res) {
+ const resources = parseRom(prg, res);
+ dispatch({
+ type: 'initialised',
+ rom: { prg, res, resources },
+ });
+
+ // Redirect to the first room.
+ navigate('/rooms/1');
}
- }, [acceptedFiles, rom, res, onFile]);
+ }, [acceptedFiles, prg, res, dispatch, navigate]);
return (
diff --git a/src/containers/ResourceExplorer.js b/src/containers/ResourceExplorer.js
index cfcdc4e..a659ceb 100644
--- a/src/containers/ResourceExplorer.js
+++ b/src/containers/ResourceExplorer.js
@@ -1,4 +1,5 @@
import { Routes, Route } from 'react-router-dom';
+import { useRom } from '../contexts/RomContext';
import RoomsContainer from './RoomsContainer';
import GfxContainer from './GfxContainer';
import PrepositionsContainer from './PrepositionsContainer';
@@ -7,7 +8,9 @@ import TitlesContainer from './TitlesContainer';
import SettingsContainer from './SettingsContainer';
import ScriptContainer from './ScriptContainer';
-const ResourceExplorer = ({ rom, res, resources }) => {
+const ResourceExplorer = () => {
+ const { prg, res, resources } = useRom();
+
if (!resources) {
return null;
}
@@ -111,7 +114,7 @@ const ResourceExplorer = ({ rom, res, resources }) => {
path="/rom-map"
element={
}
diff --git a/src/containers/RoomsContainer.js b/src/containers/RoomsContainer.js
index eb8e442..6c12078 100644
--- a/src/containers/RoomsContainer.js
+++ b/src/containers/RoomsContainer.js
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useMatch, useParams } from 'react-router-dom';
+import { useRomDispatch } from '../contexts/RomContext';
import PrimaryColumn from '../components/PrimaryColumn';
import SecondaryColumn from '../components/SecondaryColumn';
import Main from '../components/Main';
@@ -14,6 +15,7 @@ import RoomGfx from '../components/RoomGfx';
import RoomScripts from '../components/RoomScripts';
const RoomsContainer = ({ rooms, titles, roomgfx, globdata }) => {
+ const dispatch = useRomDispatch();
const isRoom = !!useMatch('/rooms/:id');
const { id } = useParams();
const [hoveredObject, setHoveredObject] = useState(null);
@@ -54,16 +56,22 @@ const RoomsContainer = ({ rooms, titles, roomgfx, globdata }) => {
};
const updatePalette = (i, colourId) => {
- const newScreen = structuredClone(room);
+ const newPalette = structuredClone(room.palette);
if (i % 4 === 0) {
// Keep the first colours in sync.
for (let i = 0; i < 16; i += 4) {
- newScreen.palette[i] = colourId;
+ newPalette[i] = colourId;
}
} else {
- newScreen.palette[i] = colourId;
+ newPalette[i] = colourId;
}
- setRoom(newScreen);
+
+ dispatch({
+ type: 'changed-palette',
+ screenType: 'room',
+ id: currentId,
+ palette: newPalette,
+ });
};
if (room && !room.header) {
diff --git a/src/containers/TitlesContainer.js b/src/containers/TitlesContainer.js
index 2bb679d..69ba60d 100644
--- a/src/containers/TitlesContainer.js
+++ b/src/containers/TitlesContainer.js
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useMatch, useParams } from 'react-router-dom';
+import { useRomDispatch } from '../contexts/RomContext';
import PrimaryColumn from '../components/PrimaryColumn';
import Main from '../components/Main';
import RoomsList from '../components/RoomsList';
@@ -12,6 +13,7 @@ import MainHeader from '../components/MainHeader';
import ResourceMetadata from '../components/ResourceMetadata';
const TitlesContainer = ({ rooms, titles }) => {
+ const dispatch = useRomDispatch();
const isRoom = !!useMatch('/rooms/:id');
const { id } = useParams();
const [currentTab, setCurrentTab] = useState('Palettes');
@@ -33,16 +35,22 @@ const TitlesContainer = ({ rooms, titles }) => {
}, [id, titles]);
const updatePalette = (i, colourId) => {
- const newScreen = structuredClone(title);
+ const newPalette = structuredClone(title.palette);
if (i % 4 === 0) {
// Keep the first colours in sync.
for (let i = 0; i < 16; i += 4) {
- newScreen.palette[i] = colourId;
+ newPalette[i] = colourId;
}
} else {
- newScreen.palette[i] = colourId;
+ newPalette[i] = colourId;
}
- setTitle(newScreen);
+
+ dispatch({
+ type: 'changed-palette',
+ screenType: 'title',
+ id: currentId,
+ palette: newPalette,
+ });
};
if (!title) {
diff --git a/src/contexts/RomContext.js b/src/contexts/RomContext.js
new file mode 100644
index 0000000..5ee4d40
--- /dev/null
+++ b/src/contexts/RomContext.js
@@ -0,0 +1,50 @@
+import { useContext, useReducer } from 'react';
+import { createContext } from 'react';
+
+const RomContext = createContext({ prg: null, res: null, resources: null });
+const RomDispatchContext = createContext(null);
+
+const romReducer = (rom, action) => {
+ switch (action.type) {
+ case 'initialised': {
+ return action.rom;
+ }
+
+ case 'changed-palette': {
+ const newRom = structuredClone(rom);
+ if (action.screenType === 'room') {
+ newRom.resources.rooms[action.id].palette = action.palette;
+ } else if (action.screenType === 'title') {
+ newRom.resources.titles[action.id].palette = action.palette;
+ } else {
+ throw Error('Unknown screen type: ' + action.screenType);
+ }
+ return newRom;
+ }
+
+ default: {
+ throw Error('Unknown action: ' + action.type);
+ }
+ }
+};
+
+const RomProvider = ({ children }) => {
+ const [rom, dispatch] = useReducer(romReducer, {
+ prg: null, // @todo Rename to PRG.
+ res: null,
+ resources: null,
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const useRom = () => useContext(RomContext);
+const useRomDispatch = () => useContext(RomDispatchContext);
+
+export { RomProvider, useRom, useRomDispatch };
diff --git a/src/index.js b/src/index.js
index 24e7a3f..8f3f941 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,10 @@
import { StrictMode } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
+import { RomProvider } from './contexts/RomContext';
import App from './App';
+import ErrorMessage from './components/ErrorMessage';
import { setColourTheme } from './lib/colourThemeUtils';
const basename =
@@ -10,9 +13,13 @@ const basename =
const root = createRoot(document.body);
root.render(
-
-
-
+
+
+
+
+
+
+
,
);
diff --git a/src/lib/cliUtils.js b/src/lib/cliUtils.js
index fe0fb62..3f0cb6e 100644
--- a/src/lib/cliUtils.js
+++ b/src/lib/cliUtils.js
@@ -7,16 +7,10 @@ import {
getResFromCrc32,
getResFromBaseRom,
} from './getResFromCrc32.js';
-import { hex, hasNesHeader } from './utils.js';
+import { hex } from './utils.js';
+import { hasNesHeader, prependNesHeader } from './romUtils.js';
const BANK_SIZE = 0x4000;
-// prettier-ignore
-const NES_HEADER = new Uint8Array([
- 0x4e, 0x45, 0x53, 0x1a, // NES
- 0x10, 0x00, 0x12, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00,
-]);
const loadRom = async (romPath = '', baseRom = null) => {
const inputExtname = extname(romPath).toLowerCase();
@@ -85,43 +79,6 @@ const saveRom = async (romPath = '', buffer) => {
}
};
-// Overwrite part of a ROM with a buffer at a given offset.
-// Return true if some data were overwritten.
-const inject = (rom, buffer, offset = 0, length = null) => {
- if (buffer.byteLength === 0) {
- return false;
- }
-
- if (Number.isInteger(length) && buffer.byteLength > length) {
- console.warn(
- 'Injected buffer is larger than allocated space. This may result in malfunctions.',
- );
- }
-
- const view = new DataView(rom);
- const bufferView = new DataView(buffer);
- let overwrites = false;
-
- for (let i = 0; i < buffer.byteLength; i++) {
- if (view.getUint8(offset + i) !== bufferView.getUint8(i)) {
- view.setUint8(offset + i, bufferView.getUint8(i));
- overwrites = true;
- }
- }
-
- if (Number.isInteger(length)) {
- // Pad the rest of the allocated space with 0xff.
- for (let i = buffer.byteLength; i < length; i++) {
- if (view.getUint8(offset + i) !== 0xff) {
- view.setUint8(offset + i, 0xff);
- overwrites = true;
- }
- }
- }
-
- return overwrites;
-};
-
const stringifyResources = (hash, size, resources, res) => {
resources.rooms.forEach((item) => {
delete item.buffer;
@@ -142,26 +99,6 @@ const stringifyResources = (hash, size, resources, res) => {
return JSON.stringify(data, null, ' ');
};
-// Append a NES header to a PRG buffer.
-const prependNesHeader = (prg) => {
- const rom = new ArrayBuffer(NES_HEADER.length + prg.byteLength);
- const romView = new DataView(rom);
- const prgView = new DataView(prg);
-
- for (let i = 0; i < NES_HEADER.length; i++) {
- romView.setUint8(i, NES_HEADER[i]);
- }
-
- const banksNum = prg.byteLength / BANK_SIZE;
- romView.setUint16(4, banksNum, true); // Patch the NES header.
-
- for (let i = NES_HEADER.length; i < rom.byteLength; i++) {
- romView.setUint8(i, prgView.getUint8(i - NES_HEADER.length));
- }
-
- return rom;
-};
-
// Expand a PRG by adding 16 banks of memory.
const expandRom = (rom) => {
const romView = new DataView(rom);
@@ -182,4 +119,4 @@ const expandRom = (rom) => {
return newRom;
};
-export { loadRom, saveRom, inject, stringifyResources, expandRom };
+export { loadRom, saveRom, stringifyResources, expandRom };
diff --git a/src/lib/parser/parseRom.js b/src/lib/parser/parseRom.js
index 6b2b2f5..a08c90e 100644
--- a/src/lib/parser/parseRom.js
+++ b/src/lib/parser/parseRom.js
@@ -58,8 +58,8 @@ const parseRom = (arrayBuffer, res) => {
}
// The title screens are stored outside of SCUMM.
- for (let i = 0; i < res?.titleoffs?.length; i++) {
- const [offset] = res.titleoffs[i];
+ for (let i = 0; i < res?.titles?.length; i++) {
+ const [offset] = res.titles[i];
// @todo Figure out the length of the title chunks.
const buffer = arrayBuffer.slice(offset); //, offset + length);
diff --git a/src/lib/resources.js b/src/lib/resources.js
index b6454cd..b83628e 100644
--- a/src/lib/resources.js
+++ b/src/lib/resources.js
@@ -102,7 +102,7 @@ const usa = {
sprdata: [[0x2ce11, 0x2be0], [0x07f6b, 0x008a]],
charset: [[0x3f6ee, 0x0090]],
preplist: [[0x3fb5a, 0x000e]],
- titleoffs: [[0x2701, 0x0000], [0x324d, 0x0000]],
+ titles: [[0x2701, 0x0000], [0x324d, 0x0000]],
characters: {},
version: 'USA',
lang: 'en-US',
@@ -213,7 +213,7 @@ const eur = {
sprdata: [[0x2ce11, 0x2be0], [0x0be28, 0x008a]],
charset: [[0x3f724, 0x0090]],
preplist: [[0x3fb90, 0x000e]],
- titleoffs: [[0x2701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x2701, 0x0000], [0x0320f, 0x0000]],
characters: {},
version: 'Europe',
lang: 'en-GB',
@@ -319,7 +319,7 @@ const swe = {
sprdata: [[0x2c401, 0x2be0], [0x0fe6b, 0x008a]],
charset: [[0x3f739, 0x0094]],
preplist: [[0x3fba9, 0x000e]],
- titleoffs: [[0x02701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x02701, 0x0000], [0x0320f, 0x0000]],
characters: {
'<': 'ä', '[': 'é', '\\': 'å', '>': 'ö',
// The 'ù' sign is in the base tileset but
@@ -429,7 +429,7 @@ const fra = {
sprdata: [[0x2ca28, 0x2be0], [0x07e48, 0x008a]],
charset: [[0x3f739, 0x009a]],
preplist: [[0x3fbaf, 0x0010]],
- titleoffs: [[0x02701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x02701, 0x0000], [0x0320f, 0x0000]],
characters: {
'[': 'é', '<': 'à', '\\': 'è', '>': 'ç', ']': 'ê', '|': 'ô',
'{': 'î', '=': 'â', '}': 'ù', '_': 'û',
@@ -538,7 +538,7 @@ const ger = {
sprdata: [[0x2c8ee, 0x2be0], [0x0fe61, 0x008a]],
charset: [[0x3f739, 0x0096]],
preplist: [[0x3fbab, 0x000e]],
- titleoffs: [[0x02701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x02701, 0x0000], [0x0320f, 0x0000]],
characters: {
'=': 'ß', '\\': 'ä', '{': 'ö', '[': 'ü', '(': '(', ')': ')',
// The 'è' sign is in the base tileset but
@@ -648,7 +648,7 @@ const esp = {
sprdata: [[0x2c401, 0x2be0], [0x0fe67, 0x008a]],
charset: [[0x3f739, 0x0099]],
preplist: [[0x3fbae, 0x000f]],
- titleoffs: [[0x02701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x02701, 0x0000], [0x0320f, 0x0000]],
characters: {
'[': 'á', '<': 'é', '|': 'í', '>': 'ó', ']': 'ú',
'{': '¿', '}': '¡', '=': 'ñ', '_': 'ü',
@@ -757,7 +757,7 @@ const ita = {
sprdata: [[0x2c8c0, 0x2be0], [0x0fe61, 0x008a]],
charset: [[0x3f739, 0x0095]],
preplist: [[0x3fbaa, 0x0010]],
- titleoffs: [[0x02701, 0x0000], [0x0320f, 0x0000]],
+ titles: [[0x02701, 0x0000], [0x0320f, 0x0000]],
characters: {
'<': 'à', '\\': 'è', '>': 'ì', '|': 'ò', '}': 'ù',
},
@@ -870,7 +870,7 @@ const proto = {
sprdata: [[0x2ce11, 0x2be0], [0x07f6b, 0x008a]],
charset: [[0x3f6ee, 0x0090]],
preplist: [[0x3fb5a, 0x000e]],
- titleoffs: [[0x2701, 0x0000], [0x325b, 0x0000]],
+ titles: [[0x2701, 0x0000], [0x325b, 0x0000]],
characters: {},
version: 'prototype',
lang: 'en-US',
diff --git a/src/lib/romUtils.js b/src/lib/romUtils.js
new file mode 100644
index 0000000..ab633c6
--- /dev/null
+++ b/src/lib/romUtils.js
@@ -0,0 +1,131 @@
+import serialisePalette from './serialiser/serialisePalette';
+import { hex } from './utils';
+
+// Return true if an arrayBuffer has a NES header.
+const hasNesHeader = (bin) => {
+ const NES_HEADER = new Uint8Array([0x4e, 0x45, 0x53, 0x1a]);
+ const view = new DataView(bin);
+ for (let i = 0; i < NES_HEADER.length; i++) {
+ if (view.getUint8(i) !== NES_HEADER[i]) {
+ return false;
+ }
+ }
+ return true;
+};
+
+const BANK_SIZE = 0x4000;
+
+// Append a NES header to a PRG buffer.
+const prependNesHeader = (prg) => {
+ // prettier-ignore
+ const NES_HEADER = new Uint8Array([
+ 0x4e, 0x45, 0x53, 0x1a, // NES
+ 0x10, 0x00, 0x12, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ ]);
+
+ const rom = new ArrayBuffer(NES_HEADER.length + prg.byteLength);
+ const romView = new DataView(rom);
+ const prgView = new DataView(prg);
+
+ for (let i = 0; i < NES_HEADER.length; i++) {
+ romView.setUint8(i, NES_HEADER[i]);
+ }
+
+ const banksNum = prg.byteLength / BANK_SIZE;
+ romView.setUint16(4, banksNum, true); // Patch the NES header.
+
+ for (let i = NES_HEADER.length; i < rom.byteLength; i++) {
+ romView.setUint8(i, prgView.getUint8(i - NES_HEADER.length));
+ }
+
+ return rom;
+};
+
+// Overwrite part of a ROM with a buffer at a given offset.
+// Return true if some data were overwritten.
+const inject = (rom, buffer, offset = 0, length = null) => {
+ if (buffer.byteLength === 0) {
+ return false;
+ }
+
+ if (Number.isInteger(length) && buffer.byteLength > length) {
+ console.warn(
+ 'Injected buffer is larger than allocated space. This may result in malfunctions.',
+ );
+ }
+
+ const view = new DataView(rom);
+ const bufferView = new DataView(buffer);
+ let overwrites = false;
+
+ for (let i = 0; i < buffer.byteLength; i++) {
+ if (view.getUint8(offset + i) !== bufferView.getUint8(i)) {
+ view.setUint8(offset + i, bufferView.getUint8(i));
+ overwrites = true;
+ }
+ }
+
+ if (Number.isInteger(length)) {
+ // Pad the rest of the allocated space with 0xff.
+ for (let i = buffer.byteLength; i < length; i++) {
+ if (view.getUint8(offset + i) !== 0xff) {
+ view.setUint8(offset + i, 0xff);
+ overwrites = true;
+ }
+ }
+ }
+
+ return overwrites;
+};
+
+// Return a string representation of a buffer for debugging purposes.
+const debugArrayBuffer = (buffer) => {
+ const view = new DataView(buffer);
+ const s = [`Length: ${buffer.byteLength} <`];
+
+ for (let i = 0; i < buffer.byteLength; i++) {
+ s.push(hex(view.getUint8(i)));
+ }
+ if (!buffer.byteLength) {
+ s.push('empty buffer');
+ }
+
+ s.push(`>`);
+
+ return s.join(' ');
+};
+
+// Inject the new content into the PRG and return a NES ROM buffer.
+const generateRomHackFile = (prg, resources) => {
+ const screens = [...resources.titles, ...resources.rooms];
+
+ // Update screen titles and rooms palette.
+ for (let i = 0; i < screens.length; i++) {
+ const screen = screens[i];
+ const { offset, size, id } = screen.metadata;
+ if (!size) {
+ continue;
+ }
+
+ const { from, to } = screen.map.find(({ type }) => type === 'palette');
+ const buffer = serialisePalette(screen.palette);
+
+ const overwrites = inject(prg, buffer, offset + from, to - from);
+ if (overwrites) {
+ console.log('Injecting screen \x1b[33m%i\x1b[0m palette', id);
+ }
+ }
+
+ // Add the iNES header to make it playable on emulators.
+ return prependNesHeader(prg);
+};
+
+export {
+ hasNesHeader,
+ prependNesHeader,
+ inject,
+ debugArrayBuffer,
+ generateRomHackFile,
+};
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 3c31d45..fb27483 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -26,16 +26,4 @@ const formatPercentage = (percent, decimals = 2) => {
return `${(percent * 100).toFixed(dm)}%`;
};
-// Return true if an arrayBuffer has a NES header.
-const hasNesHeader = (bin) => {
- const NES_HEADER = new Uint8Array([0x4e, 0x45, 0x53, 0x1a]);
- const view = new DataView(bin);
- for (let i = 0; i < NES_HEADER.length; i++) {
- if (view.getUint8(i) !== NES_HEADER[i]) {
- return false;
- }
- }
- return true;
-};
-
-export { zeroPad, hex, formatBytes, formatPercentage, hasNesHeader };
+export { zeroPad, hex, formatBytes, formatPercentage };