@chicowall/grf-loader
v1.0.13
Published
A loader for GRF files (Ragnarok Online game file)
Maintainers
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).
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-loaderQuick 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 browserKorean 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 100Validate All GRFs in a Folder
npm run validate:all -- path/to/grf/folder autoOutput 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-directoryList 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
