Initial Commit

This commit is contained in:
2025-02-27 00:10:52 -05:00
commit 19ee5de1b9
12 changed files with 648 additions and 0 deletions

53
.github/workflows/npm-publish.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: NPM Publish
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Generate Release Notes
id: generate_notes
uses: TriPSs/conventional-changelog-action@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
skip-version-file: true
skip-commit: true
skip-tag: true
output-file: false
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.generate_notes.outputs.clean_changelog }}
draft: false
prerelease: false
publish:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
scope: '@araxiaonline'
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
mpqcli/build/
bin/
*.log
.DS_Store
extracted/*

103
README.md Normal file
View File

@@ -0,0 +1,103 @@
# mpq-tools-osx
A Node.js wrapper for manipulating MPQ archives using mpqcli. This package provides a simple interface to list, extract, and create MPQ archives commonly used in Blizzard games.
## Prerequisites
- Node.js >= 14.0.0
- CMake (for building mpqcli)
- C++ compiler (for building mpqcli)
## Installation
```bash
npm install mpq-tools-osx
```
The package will automatically build mpqcli during installation.
## Usage
```javascript
const MPQTool = require('mpq-tools');
const mpq = new MPQTool();
// List all files in an MPQ archive
const files = mpq.listFiles('path/to/archive.mpq');
console.log(`Found ${files.length} files`);
// Search for specific files
const swordSounds = mpq.searchFiles('path/to/archive.mpq', 'Sword');
console.log(`Found ${swordSounds.length} sword sound files`);
// Extract a specific file
mpq.extractFile('path/to/archive.mpq', 'path/inside/archive.txt', 'output/dir');
// Extract all files
mpq.extractAll('path/to/archive.mpq', 'output/dir');
// Create a new MPQ archive from a directory
mpq.createArchive('directory/to/archive', 2); // version 2 MPQ
```
## API
### `new MPQTool(options)`
Create a new MPQTool instance.
Options:
- `mpqcliPath`: Optional path to the mpqcli executable. By default, it uses the bundled version.
### `listFiles(mpqFile)`
List all files in an MPQ archive.
- `mpqFile`: Path to the MPQ file
- Returns: Array of file paths in the archive
### `searchFiles(mpqFile, pattern)`
Search for files in an MPQ archive using a pattern.
- `mpqFile`: Path to the MPQ file
- `pattern`: Search pattern (case-insensitive)
- Returns: Array of matching file paths
### `extractFile(mpqFile, filePath, outputDir)`
Extract a specific file from an MPQ archive.
- `mpqFile`: Path to the MPQ file
- `filePath`: Path of the file within the archive to extract
- `outputDir`: Optional output directory
- Returns: true if successful
### `extractAll(mpqFile, outputDir)`
Extract all files from an MPQ archive.
- `mpqFile`: Path to the MPQ file
- `outputDir`: Optional output directory
- Returns: true if successful
### `createArchive(directory, version)`
Create a new MPQ archive from a directory.
- `directory`: Directory to create MPQ from
- `version`: MPQ version (1 or 2, default: 2)
- Returns: true if successful
### Example command line command
```bash
$ node -e "const MPQTool = require('./lib'); const mpq = new MPQTool(); mpq.addFile('patch-C.MPQ', 'test.txt', 'custom/test.txt');"
```
## License
MIT
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

27
examples/add-files.js Normal file
View File

@@ -0,0 +1,27 @@
const MPQTool = require('../lib');
const path = require('path');
const fs = require('fs');
const mpq = new MPQTool();
// Create a test directory structure
const testDir = path.join(__dirname, 'test-files');
const customPath = path.join(testDir, 'custom', 'path');
if (!fs.existsSync(customPath)) {
fs.mkdirSync(customPath, { recursive: true });
}
// Create some test files
fs.writeFileSync(path.join(customPath, 'test1.txt'), 'Hello World!');
fs.writeFileSync(path.join(customPath, 'test2.txt'), 'Another test file');
// Create an MPQ from our directory
const outputMpq = path.join(__dirname, 'test.mpq');
console.log('Creating new MPQ archive from directory...');
const mpqPath = mpq.createArchive(testDir, 2, outputMpq);
console.log(`Created MPQ at: ${mpqPath}`);
// List files in the archive to verify
console.log('\nFiles in the archive:');
const files = mpq.listFiles(mpqPath);
files.forEach(file => console.log(file));

BIN
examples/patch-z.mpq Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
Hello World!

View File

@@ -0,0 +1 @@
Another test file

280
lib/index.js Normal file
View File

@@ -0,0 +1,280 @@
const { execSync } = require('child_process');
const path = require('path');
const os = require('os');
class MPQTool {
constructor(options = {}) {
// Get platform-specific information
const platform = os.platform();
const arch = os.arch();
// Use the pre-built binary for the current platform
const binaryName = platform === 'win32' ? 'mpqcli.exe' : 'mpqcli';
const defaultPath = path.join(__dirname, '..', 'bin', `${platform}-${arch}`, binaryName);
// Allow overriding the mpqcli path
this.mpqcliPath = options.mpqcliPath || defaultPath;
// Verify the binary exists
if (!require('fs').existsSync(this.mpqcliPath)) {
throw new Error(`mpqcli binary not found at ${this.mpqcliPath}. Make sure to run 'npm run build' first.`);
}
}
/**
* List all files in an MPQ archive
* @param {string} mpqFile - Path to the MPQ file
* @returns {string[]} Array of file paths in the archive
*/
listFiles(mpqFile) {
try {
const absolutePath = path.resolve(mpqFile);
const cmd = `${this.mpqcliPath} list "${absolutePath}"`;
const output = execSync(cmd, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
});
return output.split('\n').filter(line => line.trim());
} catch (error) {
if (error.stdout) {
// Even though it's an error, the command might have output
return error.stdout.split('\n').filter(line => line.trim());
}
throw new Error(`Failed to list files: ${error.message}`);
}
}
/**
* Extract all files from an MPQ archive
* @param {string} mpqFile - Path to the MPQ file
* @param {string} outputDir - Directory to extract files to
*/
extractAll(mpqFile, outputDir) {
try {
const fs = require('fs');
const absoluteMpqPath = path.resolve(mpqFile);
const absoluteOutputDir = path.resolve(outputDir);
// Create output directory if it doesn't exist
fs.mkdirSync(absoluteOutputDir, { recursive: true });
// Extract files
const cmd = `${this.mpqcliPath} extract -o "${absoluteOutputDir}" "${absoluteMpqPath}"`;
execSync(cmd, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
});
// Fix path separators in extracted files
const files = this.listFiles(mpqFile);
files.forEach(file => {
const extractedPath = path.join(absoluteOutputDir, file);
const normalizedPath = path.join(absoluteOutputDir, file.replace(/\\/g, path.sep));
// Skip if paths are the same
if (extractedPath === normalizedPath) return;
// Create parent directory
fs.mkdirSync(path.dirname(normalizedPath), { recursive: true });
// Move file to correct location
if (fs.existsSync(extractedPath)) {
fs.renameSync(extractedPath, normalizedPath);
}
});
} catch (error) {
throw new Error(`Failed to extract archive: ${error.message}`);
}
}
/**
* Extract specific files from an MPQ archive
* @param {string} mpqFile - Path to the MPQ file
* @param {string} filePath - Path of the file within the archive to extract
* @param {string} outputDir - Optional output directory
* @returns {boolean} Success status
*/
extractFile(mpqFile, filePath, outputDir = null) {
try {
const absolutePath = path.resolve(mpqFile);
const cmd = outputDir
? `${this.mpqcliPath} extract -o "${path.resolve(outputDir)}" -f "${filePath}" "${absolutePath}"`
: `${this.mpqcliPath} extract -f "${filePath}" "${absolutePath}"`;
execSync(cmd, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
});
return true;
} catch (error) {
throw new Error(`Failed to extract file: ${error.message}`);
}
}
/**
* Create a new MPQ archive from a directory
* @param {string} directory - Directory to create MPQ from
* @param {number} version - MPQ version (1 or 2)
* @param {string} [outputPath] - Optional path for the MPQ file. If not provided, creates it next to the directory
* @returns {string} Path to the created MPQ file
*/
createArchive(directory, version = 2, outputPath = null) {
try {
const absoluteDir = path.resolve(directory);
const dirName = path.basename(absoluteDir);
const workingDir = path.dirname(absoluteDir);
// Create the MPQ in the directory's parent
const cmd = `${this.mpqcliPath} create -v ${version} "${dirName}"`;
execSync(cmd, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
cwd: workingDir
});
// MPQ is created as directory.mpq in the working directory
const createdMpq = path.join(workingDir, dirName + '.mpq');
// Move to final destination if specified
if (outputPath) {
const finalPath = path.resolve(outputPath);
const finalDir = path.dirname(finalPath);
// Create output directory if needed
if (!require('fs').existsSync(finalDir)) {
require('fs').mkdirSync(finalDir, { recursive: true });
}
// Move the MPQ file
require('fs').renameSync(createdMpq, finalPath);
return finalPath;
}
return createdMpq;
} catch (error) {
throw new Error(`Failed to create archive: ${error.message}`);
}
}
/**
* Helper method to recursively copy a directory
* @private
*/
_copyDirectory(src, dest) {
const fs = require('fs');
const path = require('path');
// Create destination directory
fs.mkdirSync(dest, { recursive: true });
// Read directory contents
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
this._copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
/**
* Search for files in an MPQ archive using a pattern
* @param {string} mpqFile - Path to the MPQ file
* @param {string} pattern - Search pattern
* @returns {string[]} Array of matching file paths
*/
searchFiles(mpqFile, pattern) {
try {
const files = this.listFiles(mpqFile);
return files.filter(file => file.toLowerCase().includes(pattern.toLowerCase()));
} catch (error) {
throw new Error(`Failed to search files: ${error.message}`);
}
}
/**
* Add files or directories to an existing MPQ archive
* @param {string} mpqFile - Path to the MPQ file
* @param {string} sourcePath - Path to the file or directory to add
* @param {string} [targetPath] - Optional path within the MPQ where the file/directory should be placed
* @returns {boolean} Success status
*/
addToArchive(mpqFile, sourcePath, targetPath = '') {
try {
const absoluteMpqPath = path.resolve(mpqFile);
const absoluteSourcePath = path.resolve(sourcePath);
let cmd = `${this.mpqcliPath} add "${absoluteMpqPath}" "${absoluteSourcePath}"`;
if (targetPath) {
cmd += ` -p "${targetPath}"`;
}
execSync(cmd, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
});
return true;
} catch (error) {
throw new Error(`Failed to add to archive: ${error.message}`);
}
}
/**
* Add a single file to an existing MPQ archive
* @param {string} mpqFile - Path to the MPQ file
* @param {string} filePath - Path to the file to add
* @param {string} [targetPath] - Path within the MPQ where the file should be placed
* @returns {boolean} Success status
*/
addFile(mpqFile, filePath, targetPath = null) {
try {
const fs = require('fs');
const absoluteMpqPath = path.resolve(mpqFile);
const absoluteFilePath = path.resolve(filePath);
// Create a temporary directory
const tempDir = path.join(require('os').tmpdir(), 'mpq-tool-' + Date.now());
fs.mkdirSync(tempDir, { recursive: true });
// Extract current MPQ contents
console.log('Extracting current MPQ contents...');
this.extractAll(absoluteMpqPath, tempDir);
// Copy new file to temp directory
// Normalize path separators for MPQ
const finalPath = (targetPath || path.basename(absoluteFilePath))
.split(/[/\\]/) // Split on both forward and back slashes
.join('\\'); // Join with backslashes
const targetFilePath = path.join(tempDir, finalPath);
// Create parent directories if needed
fs.mkdirSync(path.dirname(targetFilePath), { recursive: true });
// Copy the file
fs.copyFileSync(absoluteFilePath, targetFilePath);
// Create new MPQ with all files
console.log('Creating new MPQ with added file...');
const tempMpq = path.join(require('os').tmpdir(), 'temp.mpq');
this.createArchive(tempDir, 2, tempMpq);
// Replace original MPQ
fs.renameSync(tempMpq, absoluteMpqPath);
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
return true;
} catch (error) {
throw new Error(`Failed to add file: ${error.message}`);
}
}
}
module.exports = MPQTool;

1
mpqcli Submodule

Submodule mpqcli added at 2dd551cbdc

88
package-lock.json generated Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "mpq-tool",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mpq-tool",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"stormlib-node": "^1.3.6"
}
},
"node_modules/fs-extra": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT"
},
"node_modules/stormlib-node": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/stormlib-node/-/stormlib-node-1.3.6.tgz",
"integrity": "sha512-LBLo1fzTRd+KOAZUmsWhKNHf8gOJ+YEiu7GTXNuXE5jWXk5abdFQOpOz1FYxW+fDXdnq9sD/t2XyQqA9SJiCJw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"fs-extra": "^11.1.1",
"node-addon-api": "^6.1.0",
"typescript": "^5.1.3"
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
}
}
}

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@araxiaonline/mpq-tools-osx",
"version": "1.0.0",
"description": "Node.js wrapper for MPQ archive manipulation for MacOSX using mpqcli",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "node scripts/build.js",
"prepare": "npm run build",
"install": "node scripts/build.js",
"postinstall": "cd mpqcli && cmake -B build && cmake --build build",
"publish": "npm publish --access public"
},
"keywords": [
"mpq",
"wow",
"warcraft",
"archive",
"stormlib"
],
"author": "ben-of-codecraft",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/araxiaonline/mpq-tools-osx"
},
"files": [
"lib/",
"bin/",
"mpqcli/",
"scripts/",
"README.md"
],
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
]
}

42
scripts/build.js Normal file
View File

@@ -0,0 +1,42 @@
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
// Get platform-specific information
const platform = os.platform();
const arch = os.arch();
// Define the build directory
const buildDir = path.join(__dirname, '..', 'bin', `${platform}-${arch}`);
// Create the build directory if it doesn't exist
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Build mpqcli
console.log(`Building mpqcli for ${platform}-${arch}...`);
try {
// Navigate to mpqcli directory
const mpqcliDir = path.join(__dirname, '..', 'mpqcli');
process.chdir(mpqcliDir);
// Build using CMake
execSync('cmake -B build', { stdio: 'inherit' });
execSync('cmake --build build', { stdio: 'inherit' });
// Copy the binary to the platform-specific directory
const binaryName = platform === 'win32' ? 'mpqcli.exe' : 'mpqcli';
const binaryPath = path.join('build', 'bin', binaryName);
const targetPath = path.join(buildDir, binaryName);
fs.copyFileSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755); // Make executable
console.log(`Successfully built mpqcli and copied to ${targetPath}`);
} catch (error) {
console.error('Failed to build mpqcli:', error.message);
process.exit(1);
}