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

@chicowall/grf-loader

v1.0.13

Published

A loader for GRF files (Ragnarok Online game file)

Readme

GRF Loader

GRF is an archive file format that supports lossless data compression used on Ragnarok Online to store game assets. A GRF file may contain one or more files or directories that may have been compressed (deflate) and encrypted (variant of DES).

roBrowser project license: MIT node browser lint

Features

  • ✅ GRF version 0x200 support
  • ✅ Works in both Node.js and browser environments
  • ✅ DES decryption support
  • Korean filename encoding (CP949/EUC-KR) with auto-detection
  • Mojibake detection and fixing
  • Case-insensitive path resolution
  • Collision-safe indexing (no lost files)
  • ✅ Memory efficient (streams data without loading entire file)
  • ❌ Custom encryption not supported

Installation

npm install @chicowall/grf-loader

Quick Start

Node.js

import { GrfNode } from '@chicowall/grf-loader';
import { openSync } from 'fs';

const fd = openSync('path/to/data.grf', 'r');
const grf = new GrfNode(fd);

await grf.load();

// Get file
const { data, error } = await grf.getFile('data\\sprite\\monster.spr');

Browser

import { GrfBrowser } from '@chicowall/grf-loader';

const file = document.querySelector('input[type="file"]').files[0];
const grf = new GrfBrowser(file);

await grf.load();

Configuration Options

const grf = new GrfNode(fd, {
  // Filename encoding: 'auto' | 'cp949' | 'euc-kr' | 'utf-8' | 'latin1'
  filenameEncoding: 'auto',

  // Auto-detection threshold for bad characters (default: 1%)
  autoDetectThreshold: 0.01,

  // Maximum uncompressed file size (default: 256MB)
  maxFileUncompressedBytes: 256 * 1024 * 1024,

  // Maximum entries allowed (default: 500,000)
  maxEntries: 500000
});

API Reference

File Operations

// Get file data
const { data, error } = await grf.getFile('data\\clientinfo.xml');

// Check if file exists (case-insensitive)
grf.hasFile('DATA\\CLIENTINFO.XML'); // true

// Get file entry metadata
const entry = grf.getEntry('data\\clientinfo.xml');
// { type, offset, realSize, compressedSize, lengthAligned, rawNameBytes }

// Resolve path (handles case-insensitivity and collisions)
const result = grf.resolvePath('DATA\\Sprite\\Test.spr');
// { status: 'found' | 'not_found' | 'ambiguous', matchedPath?, candidates? }

Search API

// Find files with multiple filters
const files = grf.find({
  ext: 'spr',              // Filter by extension
  contains: 'monster',      // Filter by substring (case-insensitive)
  endsWith: 'poring.spr',  // Filter by path ending
  regex: /^data\\sprite/,  // Filter by regex
  limit: 100               // Max results
});

// Get all files by extension (fast, uses index)
const sprites = grf.getFilesByExtension('spr');
const textures = grf.getFilesByExtension('bmp');

// List all unique extensions
const extensions = grf.listExtensions();
// ['spr', 'act', 'bmp', 'wav', ...]

// List all files
const allFiles = grf.listFiles();

Statistics

const stats = grf.getStats();
// {
//   fileCount: 203092,
//   badNameCount: 4,        // Files with encoding issues
//   collisionCount: 0,      // Normalized path collisions
//   extensionStats: Map,    // Extension -> count
//   detectedEncoding: 'cp949'
// }

// Get detected encoding
const encoding = grf.getDetectedEncoding(); // 'cp949' | 'utf-8' | ...

Encoding Utilities

import {
  isMojibake,
  fixMojibake,
  normalizeFilename,
  normalizeEncodingPath,
  countBadChars,
  hasIconvLite
} from '@chicowall/grf-loader';

// Detect mojibake (CP949 misread as Windows-1252)
isMojibake('À¯ÀúÀÎÅÍÆäÀ̽º'); // true
isMojibake('유저인터페이스');     // false

// Fix mojibake
fixMojibake('À¯ÀúÀÎÅÍÆäÀ̽º'); // '유저인터페이스'

// Normalize entire path
normalizeEncodingPath('data\\texture\\À¯ÀúÀÎÅÍÆäÀ̽º\\test.bmp');
// 'data\\texture\\유저인터페이스\\test.bmp'

// Count problematic characters
countBadChars('test�file.txt'); // 1 (U+FFFD replacement char)

// Check if iconv-lite is available (Node.js only)
hasIconvLite(); // true in Node.js, false in browser

Korean Encoding Support

GRF files from Korean Ragnarok Online clients use CP949 encoding for filenames. This library automatically detects and handles Korean encoding:

// Auto-detection (default)
const grf = new GrfNode(fd, { filenameEncoding: 'auto' });

// Force CP949
const grf = new GrfNode(fd, { filenameEncoding: 'cp949' });

// Reload with different encoding
await grf.reloadWithEncoding('euc-kr');

Encoding Detection Results

| Scenario | Detection | Result | |----------|-----------|--------| | Korean GRF | cp949 | ✅ Proper Korean display | | English GRF | utf-8 | ✅ ASCII preserved | | Mixed content | cp949 | ✅ Both work |

Error Handling

import { GrfError, GRF_ERROR_CODES } from '@chicowall/grf-loader';

try {
  await grf.load();
} catch (e) {
  if (e instanceof GrfError) {
    switch (e.code) {
      case 'INVALID_MAGIC':
        console.log('Not a GRF file');
        break;
      case 'UNSUPPORTED_VERSION':
        console.log('Only version 0x200 supported');
        break;
      case 'CORRUPT_TABLE':
        console.log('File table is corrupted');
        break;
      case 'LIMIT_EXCEEDED':
        console.log('File exceeds size limit');
        break;
    }
  }
}

Error Codes

| Code | Description | |------|-------------| | INVALID_MAGIC | File is not a GRF (invalid signature) | | UNSUPPORTED_VERSION | GRF version not 0x200 | | NOT_LOADED | GRF not loaded yet | | FILE_NOT_FOUND | Requested file not in archive | | AMBIGUOUS_PATH | Multiple files match (collision) | | DECOMPRESS_FAIL | Decompression failed | | CORRUPT_TABLE | File table is corrupted | | LIMIT_EXCEEDED | Size/count limit exceeded |

Validation Tools

Validate a Single GRF

npm run validate:grf -- path/to/data.grf auto 100

Validate All GRFs in a Folder

npm run validate:all -- path/to/grf/folder auto

Output example:

================================================================================
SUMMARY
================================================================================
GRFs loaded:        3/3
Total files:        655,144
Bad U+FFFD:         12
Bad C1 Control:     40
Read tests passed:  300
Read tests failed:  0

Encoding Health:    99.99% (655,092/655,144 clean)

Examples

Extract All Files

npx ts-node examples/extract-all.ts path/to/data.grf output-directory

List All Files by Extension

const grf = new GrfNode(fd);
await grf.load();

// Get all sprite files
const sprites = grf.getFilesByExtension('spr');
console.log(`Found ${sprites.length} sprite files`);

// Get extension statistics
const stats = grf.getStats();
for (const [ext, count] of stats.extensionStats) {
  console.log(`${ext}: ${count} files`);
}

Handle Case-Insensitive Lookups

// All of these resolve to the same file:
await grf.getFile('data\\sprite\\monster.spr');
await grf.getFile('DATA\\SPRITE\\MONSTER.SPR');
await grf.getFile('data/sprite/monster.spr');

Browser Limitations

  • iconv-lite is not available in browsers
  • CP949 extended characters may show as C1 control characters
  • Use hasIconvLite() to check availability

License

MIT