Skip to content

Instantly share code, notes, and snippets.

@Kattoor
Created October 26, 2022 17:15
Show Gist options
  • Save Kattoor/eb03c744e98eaad5a7e4b81e02ed23de to your computer and use it in GitHub Desktop.
Save Kattoor/eb03c744e98eaad5a7e4b81e02ed23de to your computer and use it in GitHub Desktop.
import {globby} from 'globby';
import workerpool from 'workerpool';
import {dirname} from 'path';
import {fileURLToPath} from 'url';
import {open} from 'yauzl';
import fs from 'fs';
const start = Date.now();
const yellow = "\x1b[33m%s\x1b[0m";
const cyan = "\x1b[36m%s\x1b[0m"
const path = 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Shatterline\\';
const __dirname = dirname(fileURLToPath(import.meta.url));
const filePaths = (await globby('**/*.pak', {cwd: path})).map((pathSuffix) => path + pathSuffix.replace(/\//g, '\\'));
console.log(filePaths.length + ' PAK files found');
let extractedCount = 0;
const recordsPromises = filePaths.map(extractRelevantRecords);
const records = [].concat(...(await Promise.all(recordsPromises)));
const groupedByPakFile =
Object.entries(
records.reduce((acc, entry) => {
if (acc[entry.pakFile] == null) {
acc[entry.pakFile] = [];
}
acc[entry.pakFile].push(entry);
return acc;
}, {}));
fs.writeFileSync('./groupedByPackFile.json', JSON.stringify(groupedByPakFile));
const pool = workerpool.pool(__dirname + '/extract-worker.js', {workerType: 'process'});
for (let [pakFilePath, fileEntries] of groupedByPakFile) {
console.log(cyan, 'Extracting ' + fileEntries.length + ' files from ' + pakFilePath.split('\\').slice(6).join('\\'));
const serializedParameters = JSON.stringify({pakFilePath, fileEntries});
pool.exec('extractFromPak', [serializedParameters])
.then(async () => {
const stats = pool.stats();
if (stats.pendingTasks === 0 && stats.activeTasks === 0) {
console.log('Finished in ' + (Date.now() - start) + 'ms');
await pool.terminate();
}
});
}
async function extractRelevantRecords(filePath) {
return new Promise(resolve => {
const entries = [];
open(filePath, {lazyEntries: true}, (err, zipFile) => {
zipFile.readEntry();
zipFile.on('entry', entry => {
if (/\.(1|2|3|4|5|6|7|8|9|dds)$/gm.test(entry.fileName)) {
entries.push({
pakFile: filePath,
offset: entry.relativeOffsetOfLocalHeader,
fileName: entry.fileName,
compressedSize: entry.compressedSize,
uncompressedSize: entry.uncompressedSize
});
}
zipFile.readEntry();
});
zipFile.once('end', () => {
console.log(yellow, 'Extracted PAK file headers ' + (++extractedCount) + ' / ' + filePaths.length + ': ' + filePath.split('\\').slice(6).join('\\'));
resolve(entries);
});
});
});
}
import ffi from 'ffi-napi';
import {promises as fs} from 'fs';
import workerpool from 'workerpool';
const magenta = "\x1b[35m%s\x1b[0m"
const outPath = 'C:\\users\\jaspe\\out\\';
const lib = ffi.Library('oo2core_8_win64.dll', {
'OodleLZ_Decompress': ['void', ['char *', 'int', 'char *', 'int', 'int', 'int', 'int', 'void *', 'void *', 'void *', 'void *', 'void *', 'void *', 'int']]
});
async function extractFromPak(serializedParameters) {
const {pakFilePath, fileEntries} = JSON.parse(serializedParameters);
for (let fileEntry of fileEntries) {
const fileHandle = await fs.open(pakFilePath, 'r');
const localHeader = Buffer.alloc(4);
await fileHandle.read({buffer: localHeader, position: fileEntry.offset + 26});
const fileNameLength = localHeader.readUInt16LE(0);
const extraFieldLength = localHeader.readUInt16LE(2);
const compressedData = Buffer.alloc(fileEntry.compressedSize);
await fileHandle.read({
buffer: compressedData,
position: fileEntry.offset + 30 + fileNameLength + extraFieldLength
});
await fileHandle.close();
if (fileEntry.compressedSize === fileEntry.uncompressedSize) {
await saveFile(fileEntry.fileName, compressedData);
} else {
const uncompressedData = Buffer.alloc(fileEntry.uncompressedSize);
lib.OodleLZ_Decompress(compressedData, fileEntry.compressedSize, uncompressedData, fileEntry.uncompressedSize, 0, 0, 0, null, null, null, null, null, null, 3);
const [b1, b2, b3] = uncompressedData;
if (b1 === 0xef && b2 === 0xbb && b3 === 0xbf) {
await saveFile(fileEntry.fileName, uncompressedData.slice(3));
} else {
await saveFile(fileEntry.fileName, uncompressedData);
}
}
}
console.log(magenta, 'Finished ' + pakFilePath.split('\\').slice(6).join('\\'));
}
async function saveFile(path, out) {
const absolutePath = (outPath + path).replace(/\//g, '\\');
const directory = absolutePath.slice(0, absolutePath.lastIndexOf('\\'));
await fs.mkdir(directory, {recursive: true});
await fs.writeFile(absolutePath, out);
}
workerpool.worker({
extractFromPak: extractFromPak
});
{
"name": "shatterline-datamining",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ffi-napi": "^4.0.3",
"globby": "^13.1.2",
"workerpool": "^6.2.1",
"yauzl": "https://github.com/Kattoor/yauzl.git"
},
"type": "module"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment