# @kokomi/w3g-parser-browser

> A native JavaScript WarCraft 3 replay parser implementation. Browser implementation

Latest version **0.1.0** (published 2026-09-08) · MIT license · 0 weekly downloads

## Install

```sh
npm install @kokomi/w3g-parser-browser
pnpm add @kokomi/w3g-parser-browser
yarn add @kokomi/w3g-parser-browser
bun add @kokomi/w3g-parser-browser
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; no esm support; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.0 |
| Published | 2026-09-08 |
| First published | 2022-09-30 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | CommonJS |
| Dependencies | 1 |
| Unpacked size | 106.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Author | Kokomi |
| Maintainers | kokomi |

## Links

- npm: https://www.npmjs.com/package/@kokomi/w3g-parser-browser
- Repository: https://github.com/kirill-782/w3g-replay-parser-browser
- Homepage: https://github.com/kirill-782/w3g-replay-parser-browser#readme
- Issues: https://github.com/kirill-782/w3g-replay-parser-browser/issues
- npm.io page: https://npm.io/package/@kokomi/w3g-parser-browser

## Dependencies (1)

- [pako](https://npm.io/package/pako.md) ^2.0.4

## Recent versions

- 0.1.0 (latest) — 2026-09-08
- 0.0.18 — 2023-02-09
- 0.0.17 — 2023-02-09
- 0.0.16 — 2022-11-30
- 0.0.15 — 2022-11-30
- 0.0.14 — 2022-11-15
- 0.0.13 — 2022-11-15
- 0.0.12 — 2022-11-15
- 0.0.11 — 2022-11-15
- 0.0.10 — 2022-11-15
- 0.0.9 — 2022-11-15
- 0.0.8 — 2022-10-07
- 0.0.7 — 2022-10-07
- 0.0.6 — 2022-10-02
- 0.0.5 — 2022-10-02
- … 4 more at https://npm.io/package/@kokomi/w3g-parser-browser/versions

## README

# @kokomi/w3g-parser-browser

A native JavaScript WarCraft 3 replay parser. Feed it a `.w3g` file, get a structure back.
No Node built-ins are used, so it runs in the browser as well as in Node.

It is a port of the Java reference parser in `irinabot/api/stats-api/replay-parser`, and is
verified against it: both implementations are run over the same replay corpus and their
output is compared field by field.

## Install

From the registry, if the package is published:

```sh
npm install @kokomi/w3g-parser-browser
```

Straight from the repository — `dist/` is not committed, so the `prepare` script builds it during
install (npm installs the dev dependencies for that automatically):

```sh
npm install git+ssh://git@github.com/kirill-782/w3g-replay-parser-browser.git
npm install github:kirill-782/w3g-replay-parser-browser#master   # or a tag/commit
```

From a checkout on the same machine. This symlinks the directory, so a rebuild here is picked up by
the consumer immediately — handy while developing both sides:

```sh
npm install "../w3g-replay-parser-browser"   # records file:../w3g-replay-parser-browser
```

Or as a tarball, when the consumer must not reach the network or the repo:

```sh
npm pack                                     # -> kokomi-w3g-parser-browser-<version>.tgz
npm install ../path/to/kokomi-w3g-parser-browser-0.1.0.tgz
```

All four give the same package: `dist/lib` (CommonJS, ES2020) plus `dist/types`, with `pako` as the
only runtime dependency.

## Parsing a replay

```js
import ReplayParser, { ActionParser } from "@kokomi/w3g-parser-browser";

const replay = new ReplayParser().parseReplay(fileBytes);

replay.header;             // block layout of the file
replay.subHeader;          // product, version, build, game length
replay.isReforged;         // 12 byte block headers?
replay.records.gameInfo;   // host player, game name, decoded stat string
replay.records.startInfo;  // slots, random seed, game mode
replay.records.players;    // player records
replay.records.chatMessages;
replay.records.playerLeave;
replay.records.actions;    // time slots, each with rawData
replay.records.records;    // every record in stream order
replay.records.duration;   // in-game length in milliseconds
```

Time slots carry the timestamp at which the slot *starts*; `duration` is the sum of all
increments.

### Actions

Time-slot payloads are decoded separately, because most callers only need a few action types:

```js
const actionParser = new ActionParser();

for (const slot of replay.records.actions) {
  for (const block of actionParser.processActionData(slot.rawData)) {
    block.playerId;
    block.actions;          // decoded actions, each with a numeric `type`
    block.remainingBuffer;  // bytes that could not be decoded, normally empty
    block.error;            // set when a handler failed
  }
}
```

`type` is the action id from the replay format. Every id the reference parser knows is
supported, including the UJAPI ranges:

| Range | Meaning |
|-------|---------|
| `0x01`–`0x75` | standard game actions |
| `0xA0` | legacy UJAPI container, `subType` + `subAction` |
| `0xA1`–`0xB4` | standalone UJAPI actions (new id = `0xA0` + old sub id) |

The `UJApiActionType` and `UJApiSubActionType` enums name them.

Pass your own handlers to override or extend the table:

```js
new ActionParser({ 0x10: (bb, actionId) => ({ type: actionId, raw: bb.readBytes(14).toBuffer() }) });
new ActionParser(undefined, { strict: true }); // rethrow instead of collecting into remainingBuffer
```

### 64 bit values

Object, widget and player handles are 64 bit. They are exposed as a `Handle64` (`{ low, high }`,
both unsigned) so that the parsed output stays JSON serialisable:

```js
import { handle64ToHex, handle64ToBigInt } from "@kokomi/w3g-parser-browser";

handle64ToHex(action.widgetHash);    // "000003500000034e"
handle64ToBigInt(action.widgetHash); // 3642132267854n
```

The older two-field style (`objectId1` / `objectId2`) is kept on the actions that already used it.

### Stat strings

`statString.mapPath` and `statString.creator` are decoded as UTF-8, which is lossy for the cp1251
and GBK map names that are common in practice. `mapPathRaw` and `creatorRaw` carry the original
bytes, and `assemblyStatString` prefers them, so a stat string round-trips byte for byte.

### Save games

```js
import { SaveGameParser } from "@kokomi/w3g-parser-browser";

const save = new SaveGameParser().parseSaveGame(fileBytes);
save.data.mapPath;
save.data.gameName;
save.data.statString;  // decoded, same shape as replay.records.gameInfo.statString
save.data.slots;
```

### Custom decompression

pako is used by default. To use `DecompressionStream`, a native binding or a pre-warmed
instance, pass your own:

```js
new ReplayParser({ decompressor: (data) => myInflateRaw(data) });
```

## Development

```sh
npm run build   # tsc -> dist/
npm test        # builds, then runs test/run.js
```

The test suite runs on synthetic byte fixtures taken from the reference implementation and
from `docs/new-actions-0xA1-0xB4.md`. It additionally parses every `.w3g` and `.w3z` file in
`public/` if that directory exists, asserting that no action byte is left undecoded. `public/`
is git-ignored, so drop your own replays there.

## Compatibility notes for 0.1.0

Behaviour that changed relative to 0.0.x, in each case to match the reference implementation:

- **Reforged detection.** `isReforged` now tests the build number (`>= 6089`, excluding 52240)
  instead of `version > 31`. The old test misclassified 1.31 replays (build 6072, version 10031)
  as Reforged and failed to parse them at all.
- **Time slot timestamps.** A time slot is now stamped with the time it starts at. Previously the
  increment was added first, so every timestamp — including those on chat and leave records — was
  one increment too late.
- **Action ids 0x65 / 0x66 / 0x67.** Previously `0x65` was unregistered, and `0x66` and `0x67`
  reported `type` `0x65` and `0x66`. Each id now reports itself.
- **Action ids 0x29 and 0x2f** are decoded instead of truncating the rest of the command block.
  `0x2f` was unregistered and `0x29` was registered but always threw. (`0x29` is a zero-body cheat
  action that the Java reference table also omits — this is the one place the port deliberately
  covers more than the reference.)
- **UJAPI 0xA0.** The sub-action table now matches the reference (`0x04`–`0x14`). The previous
  build only recognised sub id `0x02`, which does not exist in the protocol, so no real replay
  ever decoded. The result shape is now `{ type, subType, subAction }`.
- **Target coordinates are floats.** Actions `0x11`, `0x12`, `0x13`, `0x14` and the minimap ping
  `0x68` read their coordinates with `readFloat32` instead of `readUint32`. The bytes are IEEE-754
  world coordinates, so the previous values were raw bit patterns: a click at `-2144` was reported
  as `3305504768`. Verified against a 13 replay corpus — 19 766 coordinates, all of them inside the
  map bounds implied by the stat string, versus none before. Note the map bounds are not a hard
  limit: the camera bounds are wider than the playable area, so a legitimate order can land outside.
- **Record `0x23` is decoded.** GHost calls it `REPLAY_DESYNC`; it is an 11 byte record that appears
  just before a `LeaveGame` record. It used to hit the `default` branch of the record switch and
  throw `Unknown recordId 35`, which is not a `BufferUnderflowError` and therefore aborted the whole
  parse — one rare record cost you the entire replay. The four fields are exposed raw, since the
  format documentation never established what they mean.
- **Reforged player profiles are decoded.** A `0x39` record with `subType === 3` carries a protobuf
  message with the player id, the Battle.net tag and the portrait id. It is now exposed as
  `record.profile` (`rawData` is still there) so consumers get `Stormrage55#2459` instead of a byte
  array. The message is walked directly rather than pulling in a protobuf runtime, and an
  unreadable payload leaves `profile` undefined instead of failing the parse. Reforged repeats the
  record whenever a profile changes, so the last one for a player wins.
- **UTF-8 BOM.** A leading BOM in a string is preserved instead of being silently dropped.
- **Signedness.** Hashtable keys, `frameEventId` and handle-typed sync values are read as signed
  int32, matching the reference. Previously the legacy `0xA0` sub-actions reported them unsigned
  while the equivalent standalone actions reported them signed, so the same hashtable cell did not
  compare equal across the two encodings.
- **`ActionCommandBlock.remainingBuffer`** is the new spelling; `remaingBuffer` is kept as an alias.
- **`ByteBuffer.ensureCapacity(n)`** now takes an absolute size rather than a count relative to
  `offset`. The two differ only when `offset` is non-zero.
- Build target is ES2020.

## Error handling

A replay that cannot be decoded raises, rather than returning a plausible-looking empty result:

- A malformed game stat string is a hard error. Previously it was mistaken for "this record is not
  complete yet", and `parseReplay` returned zero records and no `gameInfo` with no error at all.
- `parseBlocks` raises if it consumed every block and decoded no records.

Inside a time slot, decoding is best-effort, because one bad command block should not cost you the
rest of the replay:

- An unknown action id, a handler that fails, or a block that declares more bytes than are present
  leaves the undecoded bytes in `remainingBuffer` and sets `error`. A trailing fragment too short to
  be a block header comes back the same way instead of throwing.
- Pass `{ strict: true }` to rethrow instead. The thrown `ActionFormatError` carries `playerId`,
  `actionId`, `cause` and the blocks decoded so far.

`ByteBuffer` raises `BufferUnderflowError` (a `RangeError` subclass) when a read needs more bytes
than are present; the record layer uses it to mean "wait for the next block".

## Known gaps

Two action ids appear in real replays that neither this parser nor the Java reference decodes:
`0x7B` (mostly in Reforged replays) and `0x78`. They surface as `remainingBuffer` content rather
than being silently skipped. Across a 189 replay corpus they account for ~21 KB of undecoded bytes,
all in two files.

`AvailableActionData` lists every action shape, but `ActionCommandBlock.actions` is typed
`ActionData[]`, whose index signature means `switch (action.type)` does not narrow. Cast to the
specific interface when you need the fields typed.

---
_Source: https://npm.io/package/@kokomi/w3g-parser-browser · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
