rofl-parser
v26.16.1
Published
Decode League of Legends ROFL2 replays with patch-specific executable analysis
Readme
rofl-parser
rofl-parser decodes League of Legends ROFL2 replay files into JSON. Normal users download prebuilt decoder artifacts selected by the SHA-256 of the matching League of Legends.exe; replay decoding itself is pure JavaScript.
The executable is only read as a file. It is never launched, injected into, attached to, or modified.
Requirements
- Node.js 20 or later
- A ROFL2 replay
- A matching Windows x64
League of Legends.exePython and Unicorn are builder-only dependencies. They are not required by applications that consume a published decoder registry.
What "decoded" means
The package has three separate decoding layers:
- ROFL container decoding reads headers, metadata, compressed chunks, timestamps, packet IDs, the block-level
param, and the original payload bytes. - Structural payload decoding uses executable-specific AOT decoders to split a payload into typed values. Unknown fields use names such as
field_0x90; undecoded bytes remain available asunparsedTailor the originalpayload. - Semantic interpretation applies patch-specific, sample-verified names such as
damage,sourceNetId, orcurrentGold. These values appear undernamedParameters.
Structural success and semantic understanding are different. A payload may be fully consumed while some fields still have no known gameplay meaning.
Versioning and current support
The npm package version follows Riot's public year-based patch number. Package 26.16.1
targets League patch 26.16, whose executable and ROFL headers use the internal 16.16.*
version. The final semver component is reserved for parser-only fixes within the same game patch.
Bundled patch support:
| Riot patch | EXE/ROFL patch | Support | |---|---|---| | 26.15 | 16.15 | Container, structural decoders, movement, and the verified semantic subset below | | 26.16 | 16.16 | Container and packet identity, 75 generated structural decoders, three verified modifiers, and the recovered custom movement stream |
The verified semantic subset currently provides:
| Area | Status | Available data |
|---|---|---|
| ROFL2 container, chunks, and raw payload preservation | Implemented | Metadata, raw game-data packet rows, snapshots, timestamps, packet IDs, param, payload bytes |
| Packet identity | Implemented for known registrations | EXE-derived packet ID/name mapping; unnamed protocol streams use PKT_CUSTOM_* names |
| Damage | Implemented for verified PKT_UnitApplyDamage_s layouts | Target NetID, source NetID, amount, and damage type |
| Champion deaths and KDA | Partial | Victim and killer IDs plus K/D/A counter updates; assists can require timestamp correlation |
| Item purchase and use | Partial | Verified item/slot/action fields where present; sale, swap, and every inventory mutation are not yet normalized |
| Spell casts | Partial | Caster, slot/hash, coordinates, and explicit target when the packet variant carries them |
| Neutral objectives and buildings | Partial | Object identity/type and direct killer fields for verified layouts; indirect or minion-attributed last hits can remain unresolved |
| Health and shields | Partial | Selected replication state and deltas; the full replication schema is not decoded |
| Champion gold | Partial | Direct current and total gold replication plus deltas for verified 16.15 fields; no estimated economy timeline |
| Champion state | Partial | Direct experience, primary/secondary resources, armor, magic resistance, movement speed, and attack-damage components for verified 16.15 replication fields |
| Level updates | Implemented for verified 16.15 layouts | Entity level, level-update type code, deterministic player generation mapping, and latest directly observed experience |
| Creep score | Partial | Direct final lane/neutral CS in metadata plus observed +1 minion-kill events; the event packet is not a complete CS timeline |
| Wards and visibility | Partial | Ward creation/removal and visibility-channel events; the detecting ward is not available in every visibility packet |
| Movement | Partial | Entity ID, speed, and waypoints for the recovered 16.15 and 16.16 custom movement streams; records rejected by the native parser remain raw |
Not currently implemented:
- complete gameplay semantics for every packet and every patch;
- a full field map for the remaining replication fields, buffs/debuffs, cooldowns, and champion stats outside the verified subset;
- complete basic-attack and shield-damage semantics;
- authoritative respawn/death-timer events for all cases;
- derived analysis such as rotations, roaming, fight summaries, win/loss causes, or player coaching.
This is a replay decoding library, so it preserves evidence and exposes verified fields instead of inventing missing values. Match analysis belongs in a consuming application.
Installation
npm install rofl-parserThe package provides official TypeScript declarations and supports both CommonJS and native
Node.js ESM through the same tested runtime entry point. Consumers do not need createRequire,
a local rofl-parser.d.ts, or an interop wrapper.
CommonJS:
const { decodeDetailed } = require('rofl-parser');ESM and TypeScript:
import {
decodeAutoDetailed,
type AutoDecodeDetails,
type DecodeAutoOptions,
type ParserProgress,
} from 'rofl-parser';
const options: DecodeAutoOptions = {
replayPath: './replays/KR-1234567890.rofl',
onProgress(progress: ParserProgress) {
if (progress.stage === 'decode-progress') {
console.log(progress.completed, progress.total);
}
},
};
const result: AutoDecodeDetails = await decodeAutoDetailed(options);Decode with an existing executable
const { decode } = require('rofl-parser');
const resultPath = await decode({
executablePath: 'C:/Riot Games/League of Legends/Game/League of Legends.exe',
replayPath: './replays/KR-1234567890.rofl',
});
console.log(resultPath);
// <current working directory>/dist/KR-1234567890decode() returns the absolute path of the generated replay directory.
Use decodeDetailed() when patch information, decoder cache information, row counts, or the selected output format are also needed.
Download the matching executable automatically
Automatic executable acquisition is provided through the optional lol-exe-downloader peer dependency.
npm install rofl-parser lol-exe-downloaderconst { decodeAuto } = require('rofl-parser');
const resultPath = await decodeAuto({
replayPath: './replays/KR-1234567890.rofl',
});decodeAuto() reads the complete game version from the ROFL header. For example, replay version 16.15.801.3452 is requested as downloader version 16.15.8013452. It does not silently replace the requested build with another build from the same patch.
Downloaded executables are stored by default at:
./.rofl-cache/executables/<full-build>/League of Legends.exeUse decodeAutoDetailed() to receive executable path, size, SHA-256, version, and cache reuse information in addition to the normal decode result.
Downloader options such as region, concurrency, retries, and signal can be passed through downloadOptions:
await decodeAuto({
replayPath: './replays/KR-1234567890.rofl',
downloadOptions: {
region: 'KR',
concurrency: 8,
},
});Output format
The default output format is compact.
await decode({
executablePath,
replayPath,
outputFormat: 'compact',
});Use verbose to retain every packet occurrence field:
await decode({
executablePath,
replayPath,
outputFormat: 'verbose',
});The output directory defaults to ./dist/<replay-name>/ relative to the calling process:
dist/KR-1234567890/
├─ metadata.json
├─ fields.json
├─ raw/
│ ├─ packets.json
│ └─ snapshots.json
├─ packets/
│ └─ <packet-name>.json
└─ snapshots/
├─ index.json
└─ packets/
└─ <packet-name>.jsonraw/packets.json preserves the original game-data payload bytes. raw/snapshots.json preserves startup, control, and keyframe payload bytes. snapshots/index.json lists snapshots chronologically, while snapshots/packets/*.json groups decoded snapshot packets by packet type. fields.json documents the generated schema in English.
Snapshot packet rows include snapshotIndex, snapshotTimestamp, and snapshotType so they can be joined back to snapshots/index.json. Compact output keeps semantic namedParameters; verbose output additionally includes the original payload and structural parameters. Each snapshot uses an isolated semantic state, so keyframe restoration data does not mutate the game-data event timeline. Snapshot rows describe state restoration at a keyframe and must not be counted as new gameplay events.
Packet files
Every file under packets/ is an object with shared packet metadata at the root:
{
"level": 2,
"chunkType": 1,
"packetId": 302,
"packetName": "PKT_UnitApplyDamage_s",
"data": []
}Parsing levels are:
0: no occurrence in the file was structurally parsed1: the payload was only partially parsed, or only some occurrences were parsed successfully2: every occurrence in the file was fully consumed by its payload decoder
The level measures structural payload consumption. A level of 2 does not guarantee that every field has a verified gameplay meaning.
In particular, level does not measure whether namedParameters is complete:
level: 0can still preserve the original payload bytes.level: 1means at least one structural value was recovered or some occurrences succeeded, but bytes or variants remain unresolved.level: 2means the decoder consumed the entire payload for every occurrence in that packet file. Fields may still be named onlyfield_0xNN.
Compact packet rows
Compact rows remain JSON objects. They omit raw and tracing fields that are already preserved elsewhere:
{
"timestamp": 43.513,
"param": "0x4000003D",
"namedParameters": {
"targetNetId": "0x4000003D",
"damage": 350,
"damageType": "physical",
"sourceNetId": "0x40000042"
}
}Compact packet rows contain only:
timestampparamnamedParameters
namedParameters is null when the packet has no verified semantic interpretation yet. Raw payload remains available in raw/packets.json; raw wire parameters remain available in verbose output.
What param means
param is a separate unsigned 32-bit value stored in each ROFL block header. It is delta-encoded by the replay format when possible and is rendered as a hexadecimal string so NetID bit patterns remain visible.
For many gameplay packets it is the primary entity/object NetID or another packet context value. It is not guaranteed to have the same meaning for every packet. It is also not:
- the packet ID;
- an array containing all payload parameters;
- a player index;
- a team ID;
- a replacement for decoding the payload.
For verified player-scoped events, compare param and decoded NetID fields with each player's paramHint or paramHintHex in metadata.json. The replay-specific replayBase/replayBaseHex explains the dynamic player entity allocation used to create those hints.
parameters, namedParameters, and payload
payloadis the original byte array from the packet. It is the lossless fallback and is always retained inraw/packets.json; verbose packet files repeat it for convenience.parametersis the ordered result of structural AOT decoding. It may contain primitive values, generatedfield_0xNNentries, or an undecoded tail. It is included in verbose packet files.namedParametersis a patch-specific semantic overlay containing only fields whose meanings have been implemented and verified. Compact packet files keep this field and omit the duplicate raw/tracing fields.
A null namedParameters value means that the original event is preserved but no trusted gameplay field names are available for that packet yet.
Champion gold
For patch 16.15, packets/CUSTOM_ReplicateFields.json includes direct champion gold state updates under namedParameters.goldUpdates:
{
"timestamp": 920.774366,
"param": "0x400000B3",
"namedParameters": {
"goldUpdates": [{
"netId": "0x400000B3",
"changedFields": ["currentGold"],
"currentGold": 840.25,
"totalGold": 5915.2734375,
"previousCurrentGold": 1740.25,
"previousTotalGold": 5915.2734375,
"currentGoldDelta": -900,
"totalGoldDelta": null
}]
}
}currentGold is the champion's spendable balance. totalGold is cumulative earned gold and does not decrease on purchases. Values are direct replay replication state, not estimates reconstructed from kills or creep score. Join netId to metadata.json player paramHint to identify the champion and player.
Gold support does not currently reconstruct every income source, shop transaction, or missed interval. Consumers should treat the emitted replication updates as direct observations and must not interpolate or estimate absent values unless their own analysis explicitly chooses to do so.
Champion state replication
Patch 16.15 also exposes verified champion state observations under namedParameters.championStateUpdates:
{
"netId": "0x400000B4",
"changedFields": ["experience", "level", "resourceValue", "resourceMax", "secondaryResourceValue", "secondaryResourceMax", "baseAttackDamage", "flatAttackDamageModifier", "percentAttackDamageModifier", "armor", "magicResist", "moveSpeed", "attackDamage"],
"experience": 0,
"level": 1,
"resourceValue": 275,
"resourceMax": 275,
"secondaryResourceValue": 100,
"secondaryResourceMax": 100,
"baseAttackDamage": 62,
"flatAttackDamageModifier": 0,
"percentAttackDamageModifier": 0,
"attackDamage": 62,
"armor": 38,
"magicResist": 32,
"moveSpeed": 345
}resourceValue/resourceMax are the primary champion resource and therefore represent mana, energy, or another champion-specific primary bar. The secondary pair represents rage, flow, ammo-style counters, or another secondary bar when present; the replay does not attach a universal resource-name enum to these values.
Experience is a direct replicated float. level is deterministically mapped from that value with the 18 standard cumulative experience thresholds and was verified against all 20 player footer levels in two replays. attackDamage is calculated from the directly replicated base, flat, and percent physical-damage fields. Armor, magic resistance, and movement speed are direct replicated values.
These are state observations, not inferred gameplay events. A missing update means the field was not observed in that packet; consumers should carry forward the previous value only when building their own state view and should not interpolate absent samples.
Champion level and creep-score observations
For patch 16.15, NPC_LevelUp.json and NPC_LevelUp_Global.json expose entityNetId, the verified new level, and the original levelUpTypeCode. When an entity generation maps deterministically to a replay player, playerEntityNetId contains the primary player NetID. latestObservedExperience and experienceObservedAt are the most recent direct replication observation available before the level packet; they are not an interpolated event-time value. A null player or experience field means that no deterministic match or prior observation was available.
S2C_IncrementMinionKills.json exposes verified minionKillsDelta: 1 events and observedIncrementCount, which counts only occurrences present in that packet stream. Validation shows that this packet is not emitted for every creep-score gain, so observedIncrementCount must not be treated as total CS. Final experience, minionsKilled, and neutralMinionsKilled values are available directly in each metadata.json player summary.
Verbose packet rows
Verbose rows contain every packet occurrence field:
{
"sourceIndex": 847,
"timestamp": 43.513,
"param": "0x4000003D",
"payload": [37, 54, 178],
"parameters": [3, 0, 350, 1],
"namedParameters": {
"damage": 350
}
}If one packet name is registered under multiple packet IDs, files are separated as <packet-name>_<packetId>.json so that the root packetId always remains accurate.
Patch compatibility
Packet IDs and payload layouts can change between executable builds. Decoder artifacts are therefore selected by the exact executable SHA-256, not by minor patch number alone.
The artifact builder can recover packet registrations and structural decoder inputs without AI. Semantic names in namedParameters are deliberately maintained separately because an executable can prove field access and types without reliably revealing the gameplay meaning of every field. New or changed layouts remain raw/structural until they are validated instead of inheriting a guessed meaning.
CLI
npx rofl-decode `
--exe "C:\Riot Games\League of Legends\Game\League of Legends.exe" `
--rofl ".\replays\KR-1234567890.rofl"Verbose output:
npx rofl-decode --exe "League of Legends.exe" --rofl replay.rofl --verboseAvailable options:
--exe <file>: executable path--rofl <file>: replay path--dist <directory>: output root; defaults to./dist--cache <directory>: decoder artifact cache; defaults to./.rofl-cache--registry <directory>: local decoder registry root--registry-url <url>: remote registryindex.jsonURL--allow-local-build: opt into the Python/Unicorn builder when no artifact exists--format compact|verbose: output format--verbose: alias for--format verbose--json: print result metadata as JSON
On success, the final absolute output path is printed to stdout.
Public API
decode(options): decode with an explicitly supplied executable and return the absolute output pathdecodeDetailed(options): decode with an explicitly supplied executable and return detailed metadatadecodeAuto(options): download the exact matching executable and return the absolute output pathdecodeAutoDetailed(options): automatically acquire the executable and return detailed metadataprepareDecoder(options): generate or reuse the executable-specific decoder artifact without decoding the replay
Caching
Decoder artifacts are keyed by patch and executable SHA-256:
./.rofl-cache/<patch>/<executable-sha256>/Resolution order is cache, bundled artifact, local registry, then remote registry. The default remote index is https://raw.githubusercontent.com/shyunku/rofl-parser/decoder-artifacts/index.json. Override it with registryUrl, or disable remote lookup with registryUrl: false. If no exact executable SHA-256 is available, decoding fails with error.code === 'DECODER_NOT_AVAILABLE'; it does not invoke Python implicitly.
The same settings can be supplied through ROFL_DECODER_REGISTRY_DIR and ROFL_DECODER_REGISTRY_URL.
Artifact builder and registry
The builder is intended for CI or a controlled maintainer machine, not application servers. It downloads the requested executable, extracts packet registrations, derives decoders, runs conservative cross-version identity recognition, validates native-derived evidence and generated JavaScript, and publishes checksum-protected static files.
npm run setup:builder
npm run build:artifacts -- --version latest --registry decoder-registrynpm run setup:builder discovers Python 3 through ROFL_PYTHON, the Windows py launcher, python3, or python. It checks every discovered interpreter and prefers one that already has Unicorn; when installation is needed it selects the explicitly configured interpreter or the newest available Python. npm run check:builder performs the same preflight without installing anything. Missing dependencies produce an actionable builder error rather than a Python import traceback.
Use a specific patch or build with --version 16.16 or --version 16.16.1234567. An existing executable can be supplied with --input and --game-version.
If a replay-only CUSTOM movement ID cannot be inherited from a prior artifact, pass the ID observed in that patch's ROFL stream once with --movement-packet-id <id>. The builder then discovers the matching deserialize function and transforms structurally; deserialize RVAs and transform constants are never supplied manually.
The included build-decoder-artifacts.yml workflow runs this pipeline on a schedule or manually and pushes the persistent static registry to the decoder-artifacts branch. Configure consumers with the raw index.json URL for that branch. New packets whose identity cannot be proved remain PKT_CUSTOM_<packetId> for later human review.
The unattended pipeline uses two separate safety gates:
- Normal packet names are inherited only from one exact decoder fingerprint match.
- Stable custom identities such as
PKT_CUSTOM_Movementmay also be inherited from one unique decoder-shape match. Decoder addresses, generated function names, and patch-specific transform tables are excluded from this shape, while object layout, field widths, selector layout, and scalar kinds remain part of it. A collision causes all candidates to remain numeric CUSTOM packets. - Semantic modifiers are stricter than packet identity. A modifier from an older patch is enabled only when the packet identity and exact decoder fingerprint both match. Shape-only identity inheritance keeps the raw packet and generated parameter decoder but withholds the older semantic modifier.
Movement is an optional artifact capability. A new patch no longer fails artifact publication merely because its movement transform changed. Packet IDs, timestamps, raw payloads, and any proven structural decoders remain usable, while validation.json reports the actual movement capability. The structural recognizer scans executable runtime functions rather than depending on a historical deserialize RVA or packet name. Once the current numeric CUSTOM packet ID is supplied or inherited, it lifts selector branches, prefix varuint termination rules, byte transforms, and storage order into a pure JavaScript decoder. The bundled 16.16 artifact resolves packet 64, deserialize 0x102ECD0, and sequential byte-vector helper 0xF8FE30 without executing the game client during replay decoding.
Each published artifact includes identity-map.json, semantic-compatibility.json, and validation.json. These record every inherited, ambiguous, unresolved, compatible, and withheld decision. Runtime decoding never guesses: an unresolved packet is emitted as raw PKT_CUSTOM_<packetId> with its payload preserved.
Maintainers can validate the success, safe-abstention, collision, and semantic-gating rules without an EXE:
npm run validate:artifactsDisclaimer
This project is not affiliated with, authorized, maintained, sponsored, or endorsed by Riot Games, Inc. League of Legends and Riot Games are trademarks or registered trademarks of Riot Games, Inc. Game executables and replay data remain subject to their applicable terms and copyrights.
