Want to read an Age of Empires II: Definitive Edition recording from JavaScript? Maybe you want to generate statistics, build an analysis application, visualize a game timeline, or turn a replay into JSON.
That was the starting point for agelens: an open-source package for parsing Age of Empires II recordings, with primary support for Definitive Edition. It works with TypeScript or JavaScript, in Node.js and directly in the browser.
The idea is simple to describe and considerably harder to implement: take a binary game file and turn it into a useful JSON structure, without relying on Python, requiring a backend, or sending the user's recording to a server.
The project is available on GitHub.
Installing agelens
npm i agelens
The package is ESM, publishes its TypeScript types, and works with Node.js 18+ and modern bundlers such as Vite, Next.js, and Webpack.
The minimal example: parsing a replay
An .aoe2record file is not JSON. It is a compressed, versioned binary format containing the game header, players, map, lobby configuration, and a long sequence of operations.
To read the complete recording, pass its bytes to parseRecording():
import { parseRecording } from "agelens";
const recording = await parseRecording(bytes);
console.log(recording.header.version);
console.log(recording.players);
console.log(recording.operations.length);
The result is an object that is safe to serialize. Here is a simplified version of its structure:
{
"header": {
"version": "DE"
},
"players": [
{
"name": "MbL40C",
"civilizationId": 19,
"teamId": 2
}
],
"operations": [
{
"id": 1,
"sequence": 0,
"offset": 1234,
"payload": {}
}
],
"warnings": []
}
A real game can contain thousands or hundreds of thousands of operations, along with actions, synchronization events, chat messages, and unknown records that the parser preserves so they can be inspected later.
Choosing the right API
Not every use case needs to load the complete game into memory. That is why agelens exposes several APIs:
| Need | API |
|---|---|
| Read the header without walking through operations | parseHeader() |
| Process operations incrementally | iterateOperations() |
| Get the complete, structured recording | parseRecording() |
| Get a JSON string directly | parseRecordingJson() |
parseHeader() is useful for building lists or metadata screens without scanning the entire game.
iterateOperations() lets you consume the operation sequence incrementally, without keeping every operation in memory at once.
parseRecording() is the most convenient option when you actually need the complete result.
This separation is not just an API design detail. It lets each integration choose how much processing and memory it needs.
Parsing an AoE2 DE recording in Node.js
For a command-line tool, processing job, or server API, the flow is short:
import { readFile, writeFile } from "node:fs/promises";
import { parseRecordingJson } from "agelens";
const input = new Uint8Array(await readFile("game.aoe2record"));
const json = await parseRecordingJson(input);
await writeFile("game.json", json);
You can also generate indented JSON for manual inspection:
const json = await parseRecordingJson(input, {}, 2);
The third argument works like the space parameter of JSON.stringify() and adds two-space indentation.
For large recordings, it is usually better to avoid this: pretty-printing can significantly increase the file size without adding any information.
Parsing recordings in the browser without uploading them
One of the project's central decisions was to make the parser work on the client side too.
Users can select or drag an .aoe2record or .mgz file, read it with the browser's standard APIs, and process it locally:
import { parseRecording } from "agelens";
const picker = document.querySelector<HTMLInputElement>("#recording")!;
picker.addEventListener("change", async () => {
const file = picker.files?.[0];
if (!file) return;
const bytes = new Uint8Array(await file.arrayBuffer());
const recording = await parseRecording(bytes);
console.log(recording.header.version);
console.log(recording.players);
});
<input id="recording" type="file" accept=".aoe2record,.mgz" />
This makes it possible to build game analyzers where the replay never leaves the player's computer.
For large files, the repository includes a static inspector that runs the processing inside a Web Worker. The worker walks through the recording body, reports the total number of operations, and keeps a preview of up to 500 of them.
That keeps the interface responsive and avoids duplicating the entire game in memory between the worker and the main page.
Current compatibility
Definitive Edition is agelens's primary target.
The repository also includes fixtures for HD Edition and Userpatch, but a new format variant or optional subsection may still appear as a warning or an unknown record.
The library tries to preserve that information instead of silently discarding it. An application can continue working with the sections it understands while identifying exactly which part of the file needs additional support.
From mgz-fast to a maintainable TypeScript library
mgz-fast already existed as a useful reference for understanding the domain and the internal structure of recordings.
agelens started as a port of that approach to TypeScript, but the work went beyond translating code. The goal became building a portable, publishable API for the JavaScript ecosystem.
The hardest problems in a binary parser tend to appear at the edges:
- Format variations between game versions.
- 64-bit Steam IDs that cannot be represented precisely by a JavaScript
number. - Malformed counters or lengths that could cause out-of-bounds reads or uncontrolled memory allocation.
- Unknown records and errors that need to preserve the file's exact offset.
The implementation ended up using a bounded BinaryReader, validation before materializing arrays, typed errors, and explicit fallbacks for unknown records.
Instead of failing with a generic error or silently ignoring bytes, an application integrating agelens can know which section failed and where in the recording the problem occurred.
Package quality also became part of the product. The repository includes ESLint, Prettier, and a test suite covering DE, HD, and Userpatch recordings, compression errors, limits, offsets, actions, and the browser inspector through Playwright.
Optimizing a parser: measure before declaring victory
It was not enough for agelens to read a replay. It also had to do so in a measurable and reproducible way.
The repository includes a benchmark with fixed fixtures that separates decompression, header reading, and operation framing. The benchmark performs a warmup, reports the median and p95, and makes it possible to compare the results with a reference implementation in Python.
Several optimizations came from measurement rather than intuition:
- Reusing a single
DataViewinside the binary reader instead of creating views for every primitive read. - Avoiding payload copies while framing operations.
- Using one raw-DEFLATE implementation compatible with both Node.js and the browser.
- Choosing between the header, incremental iteration, or the complete recording based on the information the application actually needs.
Across the two measured Definitive Edition fixtures, operation framing took approximately one quarter of the Python reference's time: between 24% and 27%.
Decompression is not where JavaScript wins on these files, and that is documented too. The results, fixtures, and command needed to reproduce them are available in the repository.
That is considerably more useful than publishing an isolated benchmark without explaining what was measured.
Turning the parser into an npm package
The final step was making it possible for someone else to use the library without cloning the repository or figuring out how to compile it manually.
That meant preparing:
- An ESM entry point for Node.js and bundlers.
- Published TypeScript types.
- Client and server examples.
- A test of the compiled package inside a clean Node project.
- An inspection of the tarball with
npm pack --dry-run.
The result is available as agelens on npm, and development continues in the open on GitHub.
If you are building an AoE2 dashboard, a build-order visualization, a game analyzer, or simply need to read an .aoe2record file from JavaScript without depending on an external service, give agelens a try.
And if you find a format variant or field that is not exposed yet, the repository is open for expanding its compatibility.
Original article in Spanish: Cómo leer archivos .aoe2record de Age of Empires II con TypeScript
Links: npm · GitHub · performance documentation