Architecture Overview: Browser-Based Game Reverse-Engineering Framework
This document describes the high-level architecture of a browser-based framework for reimplementing classic games through data-driven reverse engineering. It is agnostic to any specific game title and focuses on the reusable structural patterns.
1. Core Philosophy: Data-File-First
Section titled “1. Core Philosophy: Data-File-First”The entire project is driven by original game data files. Instead of rewriting game logic from scratch and then fitting data into it, we reverse-engineer the data formats first, build a pipeline to convert that data into web-native assets, and then write runtime code that consumes the preprocessed output.
This means the game engine never reads original binary files at runtime. All raw game data is transformed offline into JSON descriptors and PNG sprite sheets, which the browser loads like any standard web asset. The sole exception is audio, which may be decoded at runtime via WebAudio to avoid quality loss from transcoding.
The pipeline is the product. The browser engine is just a consumer of the pipeline’s output.
2. Four-Zone Directory Layout
Section titled “2. Four-Zone Directory Layout”Every project using this pattern should enforce a strict separation between four zones, each with its own lifecycle and dependency rules:
| Zone | Contents | Lifecycle | Git |
|---|---|---|---|
data/ |
Raw original game files (binaries, archives, executables) | User-supplied, never modified by code | Ignored |
tools/ |
Offline Node.js build scripts, pipeline entrypoints | Developer-run, commits new logic | Committed |
src/ |
Browser runtime code (game engine, format decoders) | Bundled by Vite, consumed in-browser | Committed |
public/assets/ |
Preprocessed pipeline output (PNGs, JSON manifests) | Regenerated by tools/ scripts |
Ignored |
Dependency Rule
Section titled “Dependency Rule”tools/ may import from src/ (to reuse format decoders in the pipeline).
src/ must never import from tools/ (no Node APIs in the browser bundle).
This is enforced at the bundler level.
Canonical identifiers (game IDs, platform IDs, shared type definitions) live in
src/ so they are available to both zones without introducing reverse
dependencies.
3. Multi-Game and Multi-Platform Identity System
Section titled “3. Multi-Game and Multi-Platform Identity System”3.1 Game Identity
Section titled “3.1 Game Identity”Each supported game title is assigned a stable string identifier. Games are independent — they may use different engines, different binary formats, and different rendering techniques. The identity system is flat (no inheritance), with provision for future engine/campaign separation if multiple titles share the same game engine with different data sets.
3.2 Platform Identity
Section titled “3.2 Platform Identity”Each game may be runnable on multiple retro platforms (Amiga OCS, DOS VGA, DOS EGA, Apple IIGS, etc.). Platforms differ in:
- Endianness: Amiga is big-endian; DOS and IIGS are little-endian.
- File naming conventions: Amiga uses mixed case; DOS/IIGS use uppercase.
- Resource type codes: DOS and IIGS may use different type codes or conventions compared to Amiga.
- Container format: Amiga uses resource forks with a standard header; DOS uses a reversed-endian variant; IIGS uses unprefixed data sections.
- Graphics subsystem: Amiga uses planar bitmaps with OCS colour palettes (12-bit, 4-bit-per-channel); DOS uses VGA byte-per-pixel indexed colour.
Platform identity is orthogonal to game identity — every game can support multiple platforms, and not all games support all platforms.
3.3 Combined Identity
Section titled “3.3 Combined Identity”A game-plus-platform pair forms a full identity (<game>/<platform>), used
everywhere: data directory lookup, asset output paths, URL parameters, and
configuration keys.
4. Data File Discovery
Section titled “4. Data File Discovery”Original game files must be placed by the user under data/<game>/<platform>/,
but subdirectory organisation is flexible. A discovery function performs a
breadth-first, case-insensitive walk up to a configurable depth, checking each
directory for expected file signatures (executables, resource files). The
shallowest match wins.
This tolerates any user-organised layout: flat files, nested subfolders, extracted disk images, WHDLoad-style slave directories, and so on.
All pipeline scripts use case-insensitive file resolution so that platform filename conventions (uppercase DOS vs. mixed-case Amiga) are transparent.
5. Configuration Table
Section titled “5. Configuration Table”A central configuration table maps every supported game/platform pair to its specifics:
| Field | Purpose |
|---|---|
| Data directory search bases | Where to look for original files |
| Executable filename | The game binary for data extraction |
| Resource file map | Which data files contain which asset types (tiles, maps, scenes, sprites, text) |
| Music flag | Whether audio data is present |
| Feature flags | Whether optional systems apply to this game (e.g. a tile/grid layer, a particular rendering mode) |
| Type code overrides | Platform-specific resource type code mappings |
| Output asset directory | Where preprocessed assets are written |
| Supported flag | Whether this combo has data available |
This table is the single source of truth for per-game, per-platform configuration. All pipeline scripts and the runtime engine read from it.
Type Boundary When the Table’s Utilities Are Extracted into a Library
Section titled “Type Boundary When the Table’s Utilities Are Extracted into a Library”If the generic lookup/discovery functions that operate on this table (data
directory resolution, config lookup, resource-type mapping, etc.) are
extracted into a shared library separate from the project consuming it, a
type-safety question arises: the game/platform identifiers are necessarily
typed as bare string in the library’s config interface, since the library
cannot know a specific project’s concrete game/platform IDs ahead of time.
If the consumer’s own configuration table is typed directly against that
same widened library interface, it silently loses the compile-time
typo-checking a string-literal-union type would otherwise provide (e.g. a
misspelled game ID would type-check without error).
Two ways to resolve this were considered:
- Make the library’s config type generic (e.g.
PlatformConfig<G extends string, P extends string>), parameterizing every function that touches it. This restores full type-safety end to end with no casts required anywhere, at the cost of permanent generic-type complexity in the library’s public API — a cost paid by every consumer, including ones who don’t need string-literal-union identifiers at all (e.g. projects loading game IDs from a runtime manifest rather than compile-time literals). - Keep the library type as plain
string, and have the consumer define its own narrowed interface locally (extending the library’s interface but overriding the identifier fields with its own string-literal-union types), typing its configuration table against that. This restores typo-checking on the table itself, at the cost of one explicit, well-scoped cast at each point where a library function hands a widened-stringresult back to consumer code that expects the narrowed type.
Decision: option 2. The library should stay generic and “dumb” about identifiers — it only ever needs some string to key its lookups — while the consumer, who is the only party that actually cares about a specific set of valid IDs, owns the narrowing. This keeps the library’s public API free of generic-type ceremony that most consumers won’t need, at the cost of a small number of clearly-commented casts localised to the consumer’s own config module.
6. The Three-Stage Pipeline
Section titled “6. The Three-Stage Pipeline”The offline build pipeline is a chain of three independent stages, each invocable separately or as part of a unified script:
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐│ Stage 1 │ │ Stage 2 │ │ Stage 3 ││ Export Game │ ──▶ │ Build Runtime │ ──▶ │ Build Music ││ Data │ │ Assets │ │ Assets ││ (executables) │ │ (data files) │ │ (audio files) │└─────────────────┘ └──────────────────┘ └──────────────────┘ data/extracted/ public/assets/ music-manifest JSON PNG + JSON + raw filesStage 1: Export Game Data
Section titled “Stage 1: Export Game Data”Parses the game executable binary (Amiga hunk format, DOS MZ/PE, etc.) to extract hardcoded data tables. The exact tables depend on the game’s genre and design, but typically include structured records such as:
- Character, unit, or actor definitions and attributes
- Item or object definitions
- Rule tables (movement costs, combat modifiers, progression curves, etc.)
- Level, region, or scene descriptors
- Any other fixed data table embedded directly in the executable
Output: Raw JSON files to data/extracted/<game>/. This is the pure
reverse-engineering step — no graphics or audio involved.
Some games may not have a parseable executable (missing, not yet reverse engineered), in which case this stage is skipped.
Stage 2: Build Runtime Assets
Section titled “Stage 2: Build Runtime Assets”Each game has a game-specific build script that produces web-native assets from the raw data files. The specific output depends on the game’s data format and content, but common asset types include:
- Tile atlases: Decode tile glyphs from data files, render into an atlas PNG, and emit frame metadata JSON.
- Map grids: Decode terrain or level map data structures into a JSON grid.
- Icon sheets: Decode sprite sheets from data files, crop into individual frames, export as PNG + JSON frame metadata.
- Backgrounds: Decode scene or screen image resources, crop to content, export as individual PNGs.
- Sprite animations: Decode animation frame data, render sprite sheets, export with frame timing metadata.
- All image/sprite resources: Generic catch-all — every image and sprite resource in the game’s data files is exported as PNG for the asset viewer.
This stage also copies the Stage 1 JSON files into the game’s asset output directory so the runtime engine can access them at a known path.
Stage 3: Build Music Assets
Section titled “Stage 3: Build Music Assets”Only runs for games with audio data. Scans the data directory for music score files, parses them to extract metadata (name, number of tracks, tempo, instrument count), and writes a music manifest JSON. No file conversion is performed — raw audio data is served to the browser and decoded at runtime via WebAudio, avoiding quality loss from transcoding.
7. Shared Format Decoder Library
Section titled “7. Shared Format Decoder Library”The format decoders are the crown jewels of the project — they represent
the reverse-engineering effort itself. They are imported by both the
offline pipeline (tools/) and the browser runtime (src/), so they must
live somewhere both zones can reach without introducing a reverse dependency.
For a single-package project, this typically means a src/assets/formats/
directory (or equivalent), imported directly by both zones. For a project
split into scoped library packages (see the framework-plan-style workspace
pattern), a format decoder becomes its own package — reusable and optionally
installable independently of the rest of the pipeline/engine, since not
every target actually needs every format (e.g. only titles using EA IFF-85
containers need an IFF decoder at all).
| Decoder | What It Reads | Key Platform Variations |
|---|---|---|
| Resource container | Archive files bundling multiple sub-resources | Big-endian, little-endian, prefixed vs unprefixed data sections |
| Bitmap image | Compressed planar/packed bitmaps | Header size, pixel layout, decompression algorithm differ by platform |
| Sprite animation | Multi-frame animation sheets | Endianness; per-frame bitplane or pixel data layout |
| Grid/level data | Grid-based layouts, where applicable (terrain, dungeon, level geometry) | Cell encoding, metadata helpers |
| Tile glyphs | Tile atlas character data | Tile dimensions, colour depths |
| Palette | Colour lookup tables | Bit-depth expansion; named palette sets |
| Scene composition | Scene description — object placement, overlays, regions | Encoding of objects, layers, and region tables |
| Music score | Music notation / tracker data | Container format, note encoding |
| Sampled sound | Audio instrument data | Synth parameters, sample data, IFF variants |
| Executable data | Game binary data tables | Executable format-specific (hunk, MZ, PE); table offset and record size conventions |
| Saved game | Save state files | Game-specific state serialisation |
| Icon catalogue | Sprite grid metadata | Mappings from icon slots to semantic entities |
Handling Game-Specific Differences
Section titled “Handling Game-Specific Differences”Shared code handles variation through several strategies:
-
Callback functions (palette selectors): The shared asset build pipeline accepts a callback that selects the correct palette for a given resource. Each game passes its own palette selection logic.
-
Platform-aware resource type resolution: A lookup function maps logical resource type names to platform-specific type codes using the configuration table’s overrides.
-
Per-game build scripts: Each game has its own Stage 2 build script that imports shared helpers but implements game-specific asset generation logic (tile rendering, map building, icon extraction).
-
Per-game viewer metadata: A configuration file declares each game’s supported asset types and recolouring mechanisms (bitplane-based palette switching vs. index-remap lookup tables).
-
Conditional runtime features: The engine checks for the existence of optional assets (music manifest, tile maps) and degrades gracefully when they are absent.
8. Browser Runtime Engine
Section titled “8. Browser Runtime Engine”The runtime engine is a PixiJS-based interactive application. It follows a standard game-loop architecture tailored to each game’s content — whether that is a strategic map, a side-scrolling level, or any other scene type:
main() ──▶ Game (Application) ├── Camera (viewport pan/zoom) ├── InputManager (keyboard + mouse) ├── AssetLoader (fetch preprocessed assets) ├── SceneRenderer (viewport-culled sprite/layer rendering) ├── EntityManager (entity rendering with contextual visuals) └── AudioManager (fetch + play via WebAudio)All rendering is viewport-culled: only visible sprites, layers, and markers are drawn, using atlas-sliced textures from preloaded sprite sheets.
Entity Rendering
Section titled “Entity Rendering”Entities are rendered using a heuristic icon or sprite selector:
- A catalogue defines the available visual representations with semantic metadata (shape, colour, faction, type).
- A matching function maps live entity data to the best available visual representation using a tiered fallback strategy.
- The result is drawn with contextual decorations (coloured backgrounds, labels, selection highlights).
This approach separates the data (what visuals exist) from the logic (how to choose one), making both independently testable.
Music and sound are decoded at runtime — the engine fetches raw audio data files from the user-supplied data directory via the dev server’s static file serving, parses them in JavaScript, and renders PCM audio to a WebAudio buffer. No transcoding step exists in the pipeline.
9. Asset Viewer
Section titled “9. Asset Viewer”A separate browser application serves as an interactive catalogue of all extracted assets:
- Browse all image and sprite resources by game and platform
- View animation frames with playback controls
- Inspect colour palettes with real-time editing
- Examine scene compositions
- Play music tracks
The viewer shares the same format decoders as the engine — it loads the preprocessed JSON manifests and PNGs produced by the pipeline, but also provides richer inspection capabilities (WebGL shader rendering for specialised colour systems, palette editors, frame-by-frame animation scrubbing).
The scaffold’s tools/viewer/ template ships a data-driven game/platform
selector, asset-type filter tabs, animation autoplay, and a generic
indexed-texture + palette WebGL2 shader with a live palette editor and
colour-cycling out of the box — see viewer.md for the full
architecture, data flow, and how a project layers a game-specific recolour
mechanism on top of the generic shader.
10. Key Architectural Principles
Section titled “10. Key Architectural Principles”-
Decode once, consume everywhere. Binary format parsers are written once, exercised by the offline pipeline, and reused by the browser engine and viewer. No duplication of reverse-engineering effort.
-
Offline transformation, runtime consumption. The pipeline converts raw game data into standard web formats (PNG, JSON). The runtime engine never touches original game binaries — it only loads preprocessed assets. This makes the engine simple, testable, and independent of the reverse-engineering work.
-
Configuration over subclassing. Game and platform differences are expressed through configuration tables, callback selectors, and per-game build scripts — not through an elaborate class hierarchy.
-
Shared identifiers, strict dependency direction. Canonical identifiers and type definitions live in the browser-safe
src/layer. The offline pipeline imports them. The reverse dependency is forbidden. -
Data drives everything. The project’s understanding of each game grows incrementally — as more formats are reverse-engineered, the pipeline extracts more data, the manifests grow richer, and the engine can render more detail. No game logic is written until the data format is understood.