Created
October 26, 2022 17:15
-
-
Save Kattoor/eb03c744e98eaad5a7e4b81e02ed23de to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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