Cómo leer archivos .aoe2record de Age of Empires II con TypeScript

¿Querés leer un recording de Age of Empires II: Definitive Edition desde JavaScript? Quizás para generar estadísticas, construir una aplicación de análisis, visualizar la línea de tiempo de una partida o transformar un replay en JSON.
Ese fue el punto de partida de agelens: un paquete open source para parsear recordings de Age of Empires II, con soporte principal para Definitive Edition. Funciona con TypeScript o JavaScript, tanto en Node.js como directamente en el navegador.
La propuesta es simple de describir y bastante más difícil de implementar: tomar el archivo binario de una partida y convertirlo en una estructura JSON útil, sin depender de Python, sin necesitar un backend y sin enviar el recording del usuario a un servidor.
El proyecto está disponible en GitHub.
Instalar agelens
npm i agelens
El paquete es ESM, publica sus tipos TypeScript y funciona en Node.js 18+ y con bundlers modernos como Vite, Next.js o Webpack.
El ejemplo mínimo: parsear un replay
Un .aoe2record no es un archivo JSON. Es un formato binario, comprimido y versionado que contiene el header de la partida, los jugadores, el mapa, la configuración del lobby y una larga secuencia de operaciones.
Para leerlo completo, alcanza con pasar sus bytes a parseRecording():
import { parseRecording } from "agelens";
const recording = await parseRecording(bytes);
console.log(recording.header.version);
console.log(recording.players);
console.log(recording.operations.length);
El resultado es un objeto seguro para serializar. Esta es una versión simplificada de su estructura:
{
"header": {
"version": "DE"
},
"players": [
{
"name": "MbL40C",
"civilizationId": 19,
"teamId": 2
}
],
"operations": [
{
"id": 1,
"sequence": 0,
"offset": 1234,
"payload": {}
}
],
"warnings": []
}
Una partida real puede contener miles o cientos de miles de operaciones, además de acciones, eventos de sincronización, mensajes de chat y registros desconocidos que el parser preserva para poder inspeccionarlos.
Elegir la API correcta
No todos los casos de uso necesitan cargar la partida completa en memoria. Por eso agelens expone varias APIs:
| Necesidad | API |
|---|---|
| Leer el header sin recorrer las operaciones | parseHeader() |
| Procesar las operaciones incrementalmente | iterateOperations() |
| Obtener el recording completo y estructurado | parseRecording() |
| Obtener directamente un string JSON | parseRecordingJson() |
parseHeader() sirve para construir listados o pantallas de metadata sin recorrer toda la partida.
iterateOperations() permite consumir la secuencia de operaciones de manera incremental, sin retenerlas todas al mismo tiempo.
parseRecording() es la opción más cómoda cuando realmente necesitás el resultado completo.
Esta separación no es solamente una cuestión de diseño de API: también permite elegir cuánto procesamiento y memoria requiere cada integración.
Parsear un recording de AoE2 DE en Node.js
Para una herramienta de línea de comandos, un job de procesamiento o una API del servidor, el flujo es corto:
import { readFile, writeFile } from "node:fs/promises";
import { parseRecordingJson } from "agelens";
const input = new Uint8Array(await readFile("partida.aoe2record"));
const json = await parseRecordingJson(input);
await writeFile("partida.json", json);
También se puede generar un JSON indentado para inspeccionarlo manualmente:
const json = await parseRecordingJson(input, {}, 2);
El tercer argumento funciona como el parámetro space de JSON.stringify() y agrega una indentación de dos espacios.
Para recordings grandes conviene evitarlo: el pretty-print aumenta considerablemente el tamaño del archivo sin agregar información.
Parsear recordings en el navegador sin subirlos a un servidor
Una de las decisiones centrales del proyecto fue que el parser también funcionara del lado del cliente.
El usuario puede seleccionar o arrastrar un archivo .aoe2record o .mgz, leerlo con las APIs estándar del navegador y procesarlo localmente:
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" />
Esto permite construir analizadores de partidas en los que el replay nunca sale de la computadora del jugador.
Para archivos grandes, el repositorio incluye un inspector estático que ejecuta el procesamiento dentro de un Web Worker. El worker recorre el cuerpo del recording, informa el total de operaciones y conserva una vista previa de hasta 500.
De esta manera, la interfaz sigue siendo responsiva y no necesita duplicar en memoria toda la partida entre el worker y la página principal.
Compatibilidad actual
Definitive Edition es el objetivo principal de agelens.
El repositorio también incluye fixtures de HD Edition y Userpatch, pero una variante nueva del formato o una subsección opcional todavía puede aparecer como warning o registro desconocido.
La librería intenta preservar esa información en lugar de descartarla silenciosamente. Una aplicación puede continuar trabajando con las secciones que entiende y, al mismo tiempo, identificar con precisión qué parte del archivo necesita soporte adicional.
De mgz-fast a una librería TypeScript mantenible
Ya existía mgz-fast, una referencia muy útil para entender el dominio y la estructura interna de los recordings.
agelens nació como un port de ese enfoque hacia TypeScript, pero el trabajo fue más allá de traducir código: el objetivo pasó a ser construir una API propia, portable y publicable para el ecosistema JavaScript.
Los problemas más difíciles de un parser binario suelen aparecer en los bordes:
- Variaciones del formato entre versiones del juego.
- IDs de Steam de 64 bits que no entran con precisión en un
number. - Contadores o longitudes malformadas que podrían provocar lecturas o asignaciones de memoria fuera de control.
- Registros desconocidos y errores que necesitan conservar el offset exacto del archivo.
La implementación terminó usando un BinaryReader acotado, validaciones antes de materializar arrays, errores tipados y fallbacks explícitos para registros desconocidos.
En lugar de fallar con un error genérico o ignorar bytes silenciosamente, la aplicación que integra agelens puede saber qué sección falló y en qué posición del recording ocurrió el problema.
La calidad del paquete también pasó a ser parte del producto. El repositorio incluye ESLint, Prettier y una suite de pruebas que cubre recordings de DE, HD y Userpatch, errores de compresión, límites, offsets, acciones y el inspector del navegador mediante Playwright.
Optimizar un parser: medir antes de declarar victoria
No alcanzaba con que agelens pudiera leer un replay. También tenía que hacerlo de una manera medible y reproducible.
El repositorio incluye un benchmark con fixtures fijos que separa las etapas de descompresión, lectura del header y framing de operaciones. El benchmark ejecuta warmup, informa mediana y p95 y permite comparar los resultados con una implementación de referencia en Python.
Varias optimizaciones surgieron de medir, no de la intuición:
- Reutilizar un único
DataViewdentro del lector binario en lugar de crear vistas para cada lectura primitiva. - Evitar copias de los payloads mientras se encuadran las operaciones.
- Utilizar una sola implementación de raw-DEFLATE compatible con Node.js y el navegador.
- Elegir entre header, iteración incremental o recording completo según la información que realmente necesita la aplicación.
En los dos fixtures de Definitive Edition medidos, el framing de operaciones tardó aproximadamente una cuarta parte del tiempo de la referencia Python: entre el 24 % y el 27 %.
La descompresión no es donde JavaScript gana en estos archivos, y eso también está documentado. Los resultados, los fixtures y el comando necesario para reproducirlos se encuentran en el repositorio.
Eso resulta bastante más útil que publicar un benchmark aislado sin explicar qué se midió.
Convertir el parser en un paquete npm
El último paso fue lograr que otra persona pudiera utilizar la librería sin clonar el repositorio ni descubrir manualmente cómo compilarla.
Eso implicó preparar:
- Una entrada ESM para Node.js y bundlers.
- Tipos TypeScript publicados.
- Ejemplos para cliente y servidor.
- Una prueba del paquete compilado dentro de un proyecto Node limpio.
- Una inspección del tarball mediante
npm pack --dry-run.
El resultado está disponible como agelens en npm y su desarrollo continúa abierto en GitHub.
Si estás construyendo un dashboard de AoE2, una visualización de build orders, un analizador de partidas o necesitás leer un .aoe2record desde JavaScript sin depender de un servicio externo, podés probar agelens.
Y si encontrás una variante del formato o un campo que todavía no está expuesto, el repositorio está abierto para seguir ampliando su compatibilidad.
Links: npm · GitHub · documentación de performance
martín
Dev, maker y eterno aprendiz. Escribo sobre código, infraestructura, herramientas y lo que voy descubriendo en el camino.
Posts relacionados
Comentarios
No hay comentarios todavía. Sé el primero.



