HQR Library
!npm packagenpm-imgnpm-url
!Build Statusbuild-imgbuild-url
!Downloadsdownloads-imgdownloads-url
!Issuesissues-imgissues-url
!Code Coveragecodecov-imgcodecov-url
!Commitizen Friendlycommitizen-imgcommitizen-url
!Semantic Releasesemantic-release-imgsemantic-release-urlA javascript HQR reader/writer for node.js and web browsers. HQR is a simple file format used by the Little Big Adventure 1 & 2 games for storing data.
A HQR file consists of a header followed by a series of entries which are either compressed or not. It can also contain hidden entries, as a mechanism to store several entries in the same index.
For more information about the content of the various HQR files found in LBA1 and LBA2 game folders, see herelba-file-info-url.
Install
npm install @lbalab/hqr
Examples
Make sure to backup your game files before using this library to mess with them.Read the palette data:
const fs = require('fs/promises');
const { HQR } = require('@lbalab/hqr');
(async () => {
const file = await fs.readFile('LBA2/RESS.HQR');
const hqr = HQR.fromArrayBuffer(
file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength)
);
console.log(hqr.entries[0].content); // main game palette as an ArrayBuffer
})();
Swap HQR entries:
const fs = require('fs/promises');
const { HQR } = require('@lbalab/hqr');
/*
** Swap regular trees with palm trees in citadel island (with rain):
** Entry #0: regular tree
** Entry #1: palm tree
*/
(async () => {
const file = await fs.readFile('LBA2/CITADEL.OBL');
const hqr = HQR.fromArrayBuffer(
file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength)
);
const tmp = hqr.entries[0];
hqr.entries[0] = hqr.entries[4];
hqr.entries[4] = tmp;
await fs.writeFile('LBA2/CITADEL.OBL', Buffer.from(hqr.toArrayBuffer()));
})();
Change an HQR entry's content:
const fs = require('fs/promises');
const { HQR } = require('@lbalab/hqr');
(async () => {
const file = await fs.readFile('LBA2/TEXT.HQR');
const hqr = HQR.fromArrayBuffer(
file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength)
);
const textBank = new Uint8Array(hqr.entries[1].content);
/* The string "Resume Game" is at offset 943.
** Let's replace the letters e => é.
*/
textBank[944] = 130;
textBank[948] = 130;
textBank[953] = 130;
fs.writeFile('LBA2/TEXT.HQR', Buffer.from(hqr.toArrayBuffer()));
})();
Create an HQR file programmatically:
const fs = require('fs/promises');
const { HQR, HQREntry, CompressionType } = require('@lbalab/hqr');
/*
** This creates a SIMPLE.HQR file from scratch that contains 3 uncompressed entries:
** #0: 512 bytes entry filled with 0s
** #1: utf-8 string: "Hello world!"
** #2: 32 bit unsigned integer with value 42 (little endian)
*/
(async () => {
const hqr = new HQR();
// Entry #0: 512 bytes filled with 0s
hqr.entries.push(new HQREntry(new ArrayBuffer(512), CompressionType.NONE));
// Entry #1: utf-8 string: "Hello world!"
const str = 'Hello world!';
hqr.entries.push(
new HQREntry(Buffer.from(str, 'utf-8'), CompressionType.NONE)
);
// Entry #2: 32 bit unsigned integer with value 42 (little endian)
const numView = new DataView(new ArrayBuffer(4));
numView.setUint32(0, 42, true);
hqr.entries.push(new HQREntry(numView.buffer, CompressionType.NONE));
await fs.writeFile('SIMPLE.HQR', Buffer.from(hqr.toArrayBuffer()));
})();
Show hidden entries information:
const fs = require('fs/promises');
const { HQR } = require('@lbalab/hqr');
(async () => {
if (process.argv.length < 3) {
console.error('Usage: node count-hidden.js <HQR file>');
process.exit(1);
}
const file = await fs.readFile(process.argv[2]);
const hqr = HQR.fromArrayBuffer(
file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength)
);
for (let i = 0; i < hqr.entries.length; i++) {
const entry = hqr.entries[i];
if (!entry) continue;
console.log(`Entry ${i} has ${entry.hiddenEntries.length} hidden entries:`);
let j = 0;
for (const hEntry of entry.hiddenEntries) {
console.log(` Hidden entry #${j}: ${hEntry.content.byteLength} bytes`);
j++;
}
}
})();
/* Example output:
$> node count-hidden.js LBA2/VOX/EN_000.VOX
Entry 154 has 4 hidden entries:
Hidden entry #0: 187692 bytes
Hidden entry #1: 156532 bytes
Hidden entry #2: 144304 bytes
Hidden entry #3: 78528 bytes
Entry 205 has 1 hidden entries:
Hidden entry #0: 139520 bytes
Entry 232 has 1 hidden entries:
Hidden entry #0: 184860 bytes
Entry 316 has 1 hidden entries:
Hidden entry #0: 116344 bytes
**/