UNPKG

grf-extractor

Version:

A command line GRF extractor, used to manage Ragnarok's .grf files.

238 lines (194 loc) 5.79 kB
// Grf full extraction var GRF = require('./Loaders/GameFile.js') var Inflate = require('./Utils/Inflate.js') var GameFileDecrypt = require('./Loaders/GameFileDecrypt.js') var EventEmitter = require('events').EventEmitter var Cluster = require('cluster') var Os = require('os') var Fs = require('fs') var Util = require('util') var Mkdirp = require('mkdirp') var Zlib = require('zlib') // Constructor var Extractor = function Constructor(grf, options) { this._progress = 0 this.grf = grf this.concurrency = options.concurrency || 100 this.output_dir = options.output || '' this.mkdirPaths = [] // List of paths already created // Start at next tick, give time for event listener to bind process.nextTick(extractStart.bind(this)) return this } // Event emmiter Util.inherits(Extractor, EventEmitter); // Start the extraction function extractStart() { var numCpus = Os.cpus().length var cluster = Cluster var grf = this.grf var concurrency = this.concurrency var extract_folder = (this.output_dir ? this.output_dir.replace(/\/?$/, '/') : '') // Add trailling slash to folder, if specified var self = this // The master only spawn workers to work on the GRF // And receives progress updates if(cluster.isMaster) { // Fork some workers for(var i = 0; i < numCpus; i++) { cluster.fork() } // Workers online var workers = [] // Separate the entries table into the number of workers var entries_slices = [{ start: 0, end: 0, }] var last = 0 var chunk_size = Math.floor(grf.entries.length / numCpus) for(var i = 0; i < numCpus; i++) { next = last + chunk_size // If last iteration, set this slice to the rest of the entries array if(i == numCpus - 1) { next = grf.entries.length } entries_slices[i] = { start: last, end: next, } last = next } // A worker is online, send it work :) cluster.on('online', function(worker) { var worker_id = workers.length // Send a slice of the entries table to this worker to process var slice = entries_slices[ worker_id ] worker.send(slice) grf.debug("Sending to worker #%d slice %d to %d.", worker_id, slice.start, slice.end) // Worker is reporting its progress worker.on('message', function(msg) { if(msg.cmd == 'progress') { self._progress++ } }) // Register worker workers.push(worker) }) self.emit('start') return } // Else is, worker // Wait for the master to send us the go signal process.on('message', function(msg) { // Receive a slice to work on var entries = grf.entries.slice(msg.start, msg.end) var next = 0 grf.debug("Worker received slice from %d to %d to work on.", msg.start, msg.end) grf.debug('Worker extracting', entries.length, 'files.') // Launch # concurrent extractors for(var i = 0; i < concurrency; i++){ extract_next() } function extract_next() { if(next >= entries.length) { return } var i = next++ var entry = entries[i] var buffer = new Buffer(entry.length_aligned); var file = entry.filename.replace( /\\/g, '/') // Replace windows back slash \ to unix forward slash var fullpath = extract_folder + file if(entry.type & GRF.FILELIST_TYPE_FILE) { // Read entry from .grf file Fs.read(grf.file.fd, buffer, 0, entry.length_aligned, entry.offset + GRF.struct_header.size, function(err, bytesRead, buffer){ if(err) { console.error("Unexpected error while reading file %s from grf", fullpath) console.error(err) finish() return } // Write entry to file self.writeEntry(entry, fullpath, buffer, finish) }) } // Directory? else { self.makeDir(fullpath, finish) } // Extract next function finish() { // Notify master of progress process.send({cmd: 'progress'}) extract_next() } } }) // Avoid workers crashing because of excpections process.on('uncaughtException', function(err) { console.error(err) }) } // Progress Extractor.prototype.progress = function progress() { return this._progress } // Make a directory Extractor.prototype.makeDir = function mkDir(path, callback) { // Create directoy if(typeof this.mkdirPaths[path] === 'undefined') { var self = this Mkdirp(path, function (err) { if(err) { console.error("Unexpected error while trying to create dir", path) console.error(err) } // Add to created paths hashtable self.mkdirPaths[path] = true callback() }) return } // Directoy already exixsts callback() } // Write entry buffer to a file Extractor.prototype.writeEntry = function writeEntry(entry, path, buffer, callback) { // Decode buffer if needed if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_MIXED) { var data = new Uint8Array(buffer) GameFileDecrypt.decodeFull( data, entry.length_aligned, entry.pack_size) buffer = new Buffer(data) } else if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_HEADER) { var data = new Uint8Array(buffer) GameFileDecrypt.decodeHeader( data, entry.length_aligned ) buffer = new Buffer(data) } var self = this // Decompress the buffer Zlib.inflate(buffer, function(err, buf) { if(err) { console.error("Unexpected error while inflating", path) console.error(err) callback() return } // Extract folder from file path var folder = path.substring(0, path.lastIndexOf("/")) self.makeDir(folder, function() { self.writeFile(path, buffer, callback) }) }) } // Write buffer to file Extractor.prototype.writeFile = function writeFile(path, buffer, callback) { Fs.writeFile(path, buffer, function (err) { if (err) { console.error("Unexpected error while writing %s to file", path) console.error(err) } callback() }) } // Exports module.exports = Extractor