npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rpgrt

v0.2.0

Published

Turn RPG Maker 2000/2003 games into diffable JSON and back – byte for byte, in pure TypeScript

Downloads

433

Readme

rpgrt

Turn RPG Maker 2000/2003 games into diffable JSON and back – byte for byte, in pure TypeScript.

When to UseInstallationCLIUsageSafetyAPI

RPG Maker 2000/2003 games – Yume Nikki, OFF, Ib, and thousands of freeware classics – store everything in the binary LCF format: maps (Map0001.lmu), the database (RPG_RT.ldb), the map tree (RPG_RT.lmt), and save files (Save01.lsd). The only production-grade implementation has been EasyRPG's liblcf – C++, a native build toolchain between you and the data, and XML as the only text output. rpgrt reimplements the format in isomorphic TypeScript (Uint8Array core, browser-safe, no native builds): LCF in, typed records or diffable JSON out, and the same bytes back.

It's built for translation work – including the step no open tool covers: writing translations back. lcftrans extracts to PO, but those catalogs are only loaded at runtime by EasyRPG Player – the game files themselves stay untranslated. rpgrt closes the loop: extract pulls every message, choice, hero name, and database string into one dump with stable addresses, and inject validates the translated dump in memory – line counts, control codes, encodability – and writes it into the actual .lmu/.ldb/.lmt files, all-or-nothing. PO export follows lcftrans catalog naming, so gettext editors and EasyRPG Player's translation workflow line up.

When to Use

| I want to… | Run | | --- | --- | | Turn an LCF file into readable, diffable JSON | convert Map0001.lmu | | Turn edited JSON back into a game file | convert Map0001.lmu.json | | Inspect or edit a save file | convert Save01.lsd | | Dump every translatable string of a game | extract ./game | | Get one dump per game file instead of one big one | extract --split | | Translate with Poedit/Weblate or lcftrans tooling | extract --po | | Write my translated dump back into the game | inject ./game strings.json (JSON or PO) | | Fix a wrong engine or codepage guess | --engine 2k3, --encoding 932 |

Installation

npm install rpgrt

# Or run once, without installing
npx rpgrt convert Map0001.lmu

Requires Node 24+ for the CLI; the codec itself runs anywhere Uint8Array does, browsers included.

CLI

# LCF → JSON, written next to the file
npx rpgrt convert Map0001.lmu

# Edit the JSON, then back to LCF
npx rpgrt convert Map0001.lmu.json
rpgrt convert <file> [options]         # .lmu/.ldb/.lmt/.lsd → JSON, or .json → LCF
rpgrt extract <game> [options]         # game directory → translatable text dump
rpgrt inject  <game> <dump> [options]  # JSON or PO dump → game files, all-or-nothing

Options:
  -o, --output <path>    convert/extract: output path (default: next to the input; strings.json for extract)
      --split            extract only: one JSON dump per game file, into strings/
      --po               extract only: lcftrans-compatible PO catalogs, into po/ (mutually exclusive with --split)
      --force            convert/extract: overwrite existing output files
      --engine <2k|2k3>  Override engine detection
      --encoding <name>  Override encoding detection (e.g. Shift_JIS or 932)

Engine and encoding are detected per game – from RPG_RT.ldb, the Encoding key in RPG_RT.ini, or charset detection over the game's text – and every command reports what it picked and why. The flags override detection when it guesses wrong.

Usage

LCF ⇄ JSON

convert writes a self-describing envelope – format, engine, and encoding travel with the data, so converting back needs no flags:

// Map0001.lmu.json
{
  "format": "lmu",
  "engine": "2k",
  "encoding": "Shift_JIS",
  "data": {
    "events": [
      { "id": 1, "name": "guard", "x": 3, "y": 4, "pages": [/* … */] }
    ]
    // …
  }
}

Edit anything – event commands, terrain, database records – and convert back. EasyRPG Player / ManiacPatch extension chunks are typed fields like everything else; chunks rpgrt genuinely doesn't know (corrupt leftovers, exotic patches) are carried through as base64 under _unknown and written back verbatim. If a file wouldn't survive the round trip byte for byte, convert says so up front instead of letting you find out after editing. Converting JSON back over an existing game file moves the previous bytes to <file>.rpgrt-bak first; overwriting an existing JSON output needs --force.

Extract → translate → inject

extract walks the whole game directory and pulls every text unit into one dump. Multi-line messages are one unit, choices keep their option lines together, and control codes like \c[3] stay verbatim:

// strings.json
{
  "engine": "2k",
  "encoding": "Shift_JIS",
  "units": [
    {
      "address": "lmu/1/events/1/pages/1/commands/0",
      "source": "むかしむかし、\nあるところに…",
      "translation": "",
      "info": ["ID 1, Page 1, Line 1, Pos (3,4)"]
    },
    {
      "address": "ldb/actors/1/name",
      "source": "アレックス",
      "translation": "",
      "context": "actors.name",
      "info": ["ID 1"]
    }
  ]
}

Fill in the translation fields – by hand, script, or machine – and inject. Units left empty stay untouched, so partial translations are fine:

npx rpgrt extract ./game -o strings.json
# … fill in "translation" fields …
npx rpgrt inject ./game strings.json

Messages may grow or shrink lines freely; a choice must keep exactly its option count, and inject tells you which unit breaks the rule.

[!TIP] For large games, extract --split writes one dump per game file (strings/Map0001.lmu.json, …) so translators can work in parallel. inject accepts the directory just the same.

PO catalogs for gettext tools

npx rpgrt extract ./game --po -o po

This writes the same catalogs lcftrans produces – RPG_RT.ldb.po (database terms), RPG_RT.ldb.common.po (common events), RPG_RT.ldb.battle.po (battle events), RPG_RT.lmt.po (map names), and one Map####.po per map – with lcftrans-style msgctxt, #. location comments, and a #: <address> reference per occurrence, ready for Poedit, Weblate, or EasyRPG Player's Language/ folder workflow.

inject accepts PO too – point it at a single .po file or a directory of them (the format is auto-detected by extension; a directory may not mix .po and .json):

npx rpgrt inject ./game po

Each entry is matched back to its game location by the #: reference rpgrt wrote; catalogs without one (from lcftrans or hand-authored) fall back to exact (msgctxt, msgid) matching scoped to the filename. #, fuzzy entries are skipped as untranslated and reported. JSON dumps remain the more direct format – one unit per game address with engine and encoding recorded, no entry merging – so reach for PO when your pipeline lives in gettext tools, and for JSON when you script against the dump.

Encodings, or: avoiding mojibake

LCF predates Unicode – text is stored in a legacy codepage with no marker in the file. Japanese games are Shift_JIS (cp932), Western ones usually Windows-1252, and reading with the wrong one turns every string into mojibake. rpgrt makes the codepage explicit: detection records it in the envelope or dump, inject re-encodes with the same codepage, and any translated character that doesn't exist in it (e.g. é in a Shift_JIS game) aborts the injection with the exact unit named – it never silently writes ?. The --encoding flag accepts both iconv names (Shift_JIS, cp1252) and bare Windows codepage numbers (932, 1252) – the numeric form EasyRPG's RPG_RT.ini uses.

Safety & limits

Round trips are byte-identical. Decoding and re-encoding an untouched file reproduces it byte for byte – defaults, chunk order, size chunks, engine quirks, and unknown chunks included. This is verified against a corpus of real 2k/2k3 games and is the foundation everything else stands on: a diff between source and converted game shows your edits, nothing else. The wire-format details and the few deliberate divergences from liblcf live in docs/serialization.md. The one exception is JSON's number model: a save-file double holding a NaN bit pattern survives the binary round trip, but JSON has no NaN – converting such a save to JSON drops the value, and convert reports the file as not byte-identical up front.

inject is all-or-nothing. Every translation is validated in memory first – unknown addresses, wrong choice line counts, changed control codes, characters the game's codepage can't represent. One failure means nothing is written, with every reason listed. The one soft case is source text that drifted since extraction (stale dump): inject warns and applies the translation anyway – re-extract and merge if that's unexpected. The writes themselves are staged beside their targets and renamed into place, so no game file is ever half-written; a crash between renames can still leave a mix of updated and original files, with each original preserved as <file>.rpgrt-bak.

Limits, so you don't discover them the hard way: rpgrt reads and writes maps, database, map tree, and save files (.lsd) – not RPG_RT.exe. convert handles save files, but extract/inject do not: saves are player state, not authored text. Maps without the canonical MapNNNN filename are skipped during extract (their units would have no stable address; extract reports each skip).

Programmatic API

import { decodeMapUnit, encodeMapUnit } from 'rpgrt'
import { createTranscoder } from 'rpgrt/encoding'

const transcoder = createTranscoder('Shift_JIS')
const mapUnit = decodeMapUnit(bytes, { engine: '2k', transcoder })
mapUnit.events[0].name = 'guard'
const encoded = encodeMapUnit(mapUnit, { engine: '2k', transcoder })

Codec (rpgrt)

Pure and isomorphic – no Node built-ins, safe to bundle for the browser. One decode/encode pair per file kind:

function decodeMapUnit(bytes: Uint8Array, options: CodecOptions): MapUnit
function encodeMapUnit(mapUnit: MapUnit, options: CodecOptions): Uint8Array
function decodeDatabase(bytes: Uint8Array, options: CodecOptions): Database
function encodeDatabase(database: Database, options: CodecOptions): Uint8Array
function decodeTreeMap(bytes: Uint8Array, options: CodecOptions): TreeMap
function encodeTreeMap(treeMap: TreeMap, options: CodecOptions): Uint8Array
function decodeSave(bytes: Uint8Array, options: CodecOptions): Save
function encodeSave(save: Save, options: CodecOptions): Uint8Array

interface CodecOptions {
  engine: '2k' | '2k3'
  /**
   * Converts between wire bytes and strings. Defaults to a lossless
   * byte↔code point mapping, so round trips stay byte-exact even
   * before the game's real encoding is known.
   */
  transcoder?: Transcoder
  /** Receives recoverable anomalies, e.g. a non-canonical file header. Silent when omitted. */
  onWarning?: (message: string) => void
}

Every record – MapUnit, Database, Actor, EventCommand, and the rest – is a generated TypeScript interface derived from liblcf's format tables, with enums to match. Decode errors throw LcfError with the record path and byte offset.

Encoding helpers (rpgrt/encoding)

/** iconv-lite-backed transcoder for a named codepage, e.g. 'Shift_JIS' or 'cp1252' */
function createTranscoder(encoding: string): Transcoder

/** Charset detection over string bytes – never returns an encoding that would corrupt the sample */
function detectEncoding(bytes: Uint8Array): string | undefined

/** Reads the EasyRPG `Encoding` key from RPG_RT.ini text */
function encodingFromIni(iniText: string): string | undefined

Credits

rpgrt's format knowledge and its generated record tables (vendor/liblcf-csv/, src/generated/) come from EasyRPG's liblcf, used under the MIT License. Thanks to the EasyRPG project.

License

MIT License © 2026-PRESENT Johann Schopplich