├── app
├── Xact
│ ├── WaveBank.js
│ ├── XactClip.js
│ ├── XactSound.js
│ ├── index.js
│ ├── SoundBank.js
│ └── AudioEngine.js
├── XnbError.js
├── Xnb
│ ├── Readers
│ │ ├── SingleReader.js
│ │ ├── Vector2Reader.js
│ │ ├── Vector4Reader.js
│ │ ├── DoubleReader.js
│ │ ├── Int32Reader.js
│ │ ├── ReflectiveReader.js
│ │ ├── BooleanReader.js
│ │ ├── UInt32Reader.js
│ │ ├── TBinReader.js
│ │ ├── Vector3Reader.js
│ │ ├── EffectReader.js
│ │ ├── BmFontReader.js
│ │ ├── CharReader.js
│ │ ├── RectangleReader.js
│ │ ├── StringReader.js
│ │ ├── ListReader.js
│ │ ├── index.js
│ │ ├── SoundEffectReader.js
│ │ ├── NullableReader.js
│ │ ├── BaseReader.js
│ │ ├── ArrayReader.js
│ │ ├── SpriteFontReader.js
│ │ ├── DictionaryReader.js
│ │ └── Texture2DReader.js
│ ├── ReaderResolver.js
│ ├── TypeReader.js
│ └── index.js
├── Enum.js
├── Struct.js
├── Presser
│ ├── index.js
│ └── Lzx.js
├── Log.js
├── BufferWriter.js
├── Porter.js
└── BufferReader.js
├── pack.bat
├── unpack.bat
├── pack.sh
├── pack.command
├── unpack.sh
├── unpack.command
├── unpacked
└── UNPACKED.md
├── packed
└── PACKED.md
├── .gitignore
├── .travis.build.sh
├── package.json
├── appveyor.yml
├── .travis.yml
├── README.md
├── LICENSE.LESSER
├── xnbcli.js
└── LICENSE.md
/app/Xact/WaveBank.js:
--------------------------------------------------------------------------------
1 |
2 | class WaveBank {
3 |
4 | }
5 |
6 | module.exports = WaveBank;
7 |
--------------------------------------------------------------------------------
/pack.bat:
--------------------------------------------------------------------------------
1 | xnbcli.exe pack ".\unpacked" ".\packed"
2 | echo Press enter to continue
3 | pause
4 |
--------------------------------------------------------------------------------
/unpack.bat:
--------------------------------------------------------------------------------
1 | xnbcli.exe unpack ".\packed" ".\unpacked"
2 | echo Press enter to continue
3 | pause
4 |
--------------------------------------------------------------------------------
/pack.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "`dirname "$0"`"
3 | ./xnbcli pack ./unpacked ./packed
4 | read -p "Press enter to continue"
5 |
--------------------------------------------------------------------------------
/pack.command:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "`dirname "$0"`"
3 | ./xnbcli pack ./unpacked ./packed
4 | read -p "Press enter to continue"
5 |
--------------------------------------------------------------------------------
/unpack.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "`dirname "$0"`"
3 | ./xnbcli unpack ./packed ./unpacked
4 | read -p "Press enter to continue"
5 |
--------------------------------------------------------------------------------
/unpack.command:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "`dirname "$0"`"
3 | ./xnbcli unpack ./packed ./unpacked
4 | read -p "Press enter to continue"
5 |
--------------------------------------------------------------------------------
/unpacked/UNPACKED.md:
--------------------------------------------------------------------------------
1 | # Packed files
2 |
3 | - Place files you want to pack in this folder.
4 |
5 | or
6 |
7 | - After running `npm run unpack`, this folder will consist of the unpacked files.
--------------------------------------------------------------------------------
/packed/PACKED.md:
--------------------------------------------------------------------------------
1 | # Packed files
2 |
3 | - Place `.xnb` files you want to unpack in this folder.
4 |
5 | or
6 |
7 | - After running `npm run pack`, this folder will consist of the packed files.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS specific
2 | *.DS_Store
3 |
4 | # NPM
5 | node_modules
6 |
7 | # Dev files
8 | /.vscode
9 | /.jshintrc
10 |
11 | # Packed/Unpacked files
12 | packed/**
13 | unpacked/**
14 |
15 | # Excluding our md files
16 | !packed/*.md
17 | !unpacked/*.md
18 |
--------------------------------------------------------------------------------
/app/XnbError.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | class XnbError extends Error {
4 | constructor(message = '') {
5 | super(message);
6 | this.name = this.constructor.name;
7 | this.message = message;
8 | Error.captureStackTrace(this, XnbError);
9 | }
10 | }
11 |
12 | module.exports = XnbError;
13 |
--------------------------------------------------------------------------------
/.travis.build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copy the node binaries into the main folder
4 | cp node_modules/lz4/build/Release/xxhash.node .
5 | cp node_modules/lz4/build/Release/lz4.node .
6 |
7 | if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
8 | pkg xnbcli.js --targets macos
9 | zip xnbcli-macos.zip xnbcli xxhash.node lz4.node packed unpacked pack.command unpack.command
10 | else
11 | pkg xnbcli.js --targets linux
12 | zip xnbcli-linux.zip xnbcli xxhash.node lz4.node packed unpacked pack.sh unpack.sh
13 | fi
14 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/SingleReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 |
4 | /**
5 | * Single Reader
6 | * @class
7 | * @extends BaseReader
8 | */
9 | class SingleReader extends BaseReader {
10 | /**
11 | * Reads Single from the buffer.
12 | * @param {BufferReader} buffer
13 | * @returns {Number}
14 | */
15 | read(buffer) {
16 | return buffer.readSingle();
17 | }
18 |
19 | write(buffer, content, resolver) {
20 | this.writeIndex(buffer, resolver);
21 | buffer.writeSingle(content);
22 | }
23 | }
24 |
25 | module.exports = SingleReader;
26 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/Vector2Reader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const SingleReader = require('./SingleReader');
4 |
5 | /**
6 | * Vector2 Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class Vector2Reader extends BaseReader {
11 | /**
12 | * Reads Vector2 from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {object}
15 | */
16 | read(buffer) {
17 | const singleReader = new SingleReader();
18 |
19 | let x = singleReader.read(buffer);
20 | let y = singleReader.read(buffer);
21 |
22 | return { x, y };
23 | }
24 | }
25 |
26 | module.exports = Vector2Reader;
27 |
--------------------------------------------------------------------------------
/app/Enum.js:
--------------------------------------------------------------------------------
1 | const Enum = values => {
2 | if (!Array.isArray(values))
3 | throw new Error('Invalid parameters, requires array of values.');
4 | if (values.length === 0)
5 | throw new Error('Invalid paremeters, empty value set.');
6 |
7 | const _enum = {};
8 | for (let i = 0; i < values.length; i++) {
9 | try {
10 | new Function(`var ${values[i]}`)();
11 | }
12 | catch (ex) {
13 | throw new Error(`Invalid paremeters, ${values[i]} is not a valid name.`);
14 | }
15 | _enum[values[i]] = Symbol();
16 | }
17 |
18 | // return a new proxy of frozen enum
19 | return Object.freeze(_enum);
20 | }
21 |
22 | module.exports = Enum;
23 |
--------------------------------------------------------------------------------
/app/Struct.js:
--------------------------------------------------------------------------------
1 | const Struct = obj => {
2 | if (typeof obj != 'object' || Array.isArray(obj))
3 | throw new Error('Invalid struct');
4 | return function (default_params = obj) {
5 | default_params = Object.assign(obj, default_params);
6 | for (let key of Object.keys(default_params))
7 | this[key] = default_params[key];
8 | return new Proxy(this, {
9 | set (target, key, value) {
10 | if (!target.hasOwnProperty(key))
11 | throw new TypeError(`Invalid property ${key}`);
12 | target[key] = value;
13 | return true;
14 | }
15 | });
16 | };
17 | };
18 |
19 | module.exports = Struct;
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xnbcli",
3 | "version": "1.0.7",
4 | "description": "XNB CLI tool for packing and unpacking XNB files.",
5 | "keywords": [
6 | "xnb"
7 | ],
8 | "scripts": {
9 | "unpack": "node ./xnbcli.js unpack ./packed ./unpacked",
10 | "pack": "node ./xnbcli.js pack ./unpacked ./packed"
11 | },
12 | "author": "James Stine",
13 | "license": "ISC",
14 | "repository": "https://github.com/LeonBlade/xnbcli",
15 | "dependencies": {
16 | "chalk": "^1.1.3",
17 | "commander": "^7.0.0",
18 | "compare-versions": "^3.6.0",
19 | "dxt-js": "0.0.3",
20 | "got": "^9.6.0",
21 | "lz4": "^0.6.5",
22 | "mkdirp": "^1.0.4",
23 | "pngjs": "^6.0.0",
24 | "walk": "^2.3.14"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/Vector4Reader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const SingleReader = require('./SingleReader');
4 |
5 | /**
6 | * Vector4 Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class Vector4Reader extends BaseReader {
11 | /**
12 | * Reads Vector4 from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {object}
15 | */
16 | read(buffer) {
17 | const singleReader = new SingleReader();
18 |
19 | let x = singleReader.read(buffer);
20 | let y = singleReader.read(buffer);
21 | let z = singleReader.read(buffer);
22 | let w = singleReader.read(buffer);
23 |
24 | return { x, y, z, w };
25 | }
26 | }
27 |
28 | module.exports = Vector4Reader;
29 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/DoubleReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 |
5 | /**
6 | * Double Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class DoubleReader extends BaseReader {
11 | /**
12 | * Reads Double from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {Number}
15 | */
16 | read(buffer) {
17 | return buffer.readDouble();
18 | }
19 |
20 | /**
21 | * Writes Double into buffer
22 | * @param {BufferWriter} buffer
23 | * @param {Mixed} data
24 | * @param {ReaderResolver}
25 | */
26 | write(buffer, content, resolver) {
27 | this.writeIndex(buffer, resolver);
28 |
29 | }
30 | }
31 |
32 | module.exports = DoubleReader;
33 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/Int32Reader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 |
5 | /**
6 | * Int32 Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class Int32Reader extends BaseReader {
11 | /**
12 | * Reads Int32 from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {Number}
15 | */
16 | read(buffer) {
17 | return buffer.readInt32();
18 | }
19 |
20 | /**
21 | * Writes Int32 and returns buffer
22 | * @param {BufferWriter} buffer
23 | * @param {Number} content
24 | * @param {ReaderResolver} resolver
25 | */
26 | write(buffer, content, resolver) {
27 | this.writeIndex(buffer, resolver);
28 | buffer.writeInt32(content);
29 | }
30 | }
31 |
32 | module.exports = Int32Reader;
33 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/ReflectiveReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 |
5 | /**
6 | * Int32 Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class ReflectiveReader extends BaseReader {
11 | /**
12 | * Reads Reflection data from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {Mixed}
15 | */
16 | read(buffer) {
17 |
18 | }
19 |
20 | /**
21 | * Writes Reflection data and returns buffer
22 | * @param {BufferWriter} buffer
23 | * @param {Number} content
24 | * @param {ReaderResolver} resolver
25 | */
26 | write(buffer, content, resolver) {
27 | this.writeIndex(buffer, resolver);
28 | buffer.writeInt32(content);
29 | }
30 | }
31 |
32 | module.exports = Int32Reader;
33 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/BooleanReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 |
5 | /**
6 | * Boolean Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class BooleanReader extends BaseReader {
11 | /**
12 | * Reads Boolean from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {Boolean}
15 | */
16 | read(buffer) {
17 | return Boolean(buffer.read(1).readInt8());
18 | }
19 |
20 | /**
21 | * Writes Boolean into buffer
22 | * @param {BufferWriter} buffer
23 | * @param {Mixed} data
24 | * @param {ReaderResolver}
25 | */
26 | write(buffer, content, resolver) {
27 | this.writeIndex(buffer, resolver);
28 | buffer.writeByte(content);
29 | }
30 | }
31 |
32 | module.exports = BooleanReader;
33 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/UInt32Reader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const ReaderResolver = require('../ReaderResolver');
3 | const BufferReader = require('../../BufferReader');
4 | const BufferWriter = require('../../BufferWriter');
5 |
6 | /**
7 | * UInt32 Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class UInt32Reader extends BaseReader {
12 | /**
13 | * Reads UInt32 from buffer.
14 | * @param {BufferReader} buffer
15 | * @returns {Number}
16 | */
17 | read(buffer) {
18 | return buffer.readUInt32();
19 | }
20 |
21 | /**
22 | *
23 | * @param {BufferWriter} buffer
24 | * @param {Number} content
25 | * @param {ReaderResolver} resolver
26 | */
27 | write(buffer, content, resolver) {
28 | this.writeIndex(buffer, resolver);
29 | buffer.writeUInt32(content);
30 | }
31 | }
32 |
33 | module.exports = UInt32Reader;
34 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | nodejs_version: "10"
3 |
4 | image:
5 | - Visual Studio 2017
6 |
7 | platform:
8 | - x86
9 | - x64
10 |
11 | cache:
12 | - node_modules
13 |
14 | install:
15 | - ps: Install-Product node $env:nodejs_version $env:platform
16 | - npm i
17 | - npm i -g pkg
18 | - pkg -t win-%PLATFORM% xnbcli.js
19 | - cp node_modules/lz4/build/Release/lz4.node .
20 | - cp node_modules/lz4/build/Release/xxhash.node .
21 | - 7z a xnbcli-windows-%PLATFORM%.zip xnbcli.exe lz4.node xxhash.node packed unpacked unpack.bat pack.bat
22 |
23 | artifacts:
24 | - path: xnbcli-windows-%PLATFORM%.zip
25 | name: xnbcli-windows
26 |
27 | build: off
28 |
29 | deploy:
30 | tag: $(appveyor_repo_tag_name)
31 | release: $(appveyor_repo_tag_name)
32 | provider: GitHub
33 | auth_token:
34 | secure: O6Rsj+hoS5X7Q1u7TjtFwjRN53ohwjF8Mik/BpVidoefAag5FfpxoWqS5GTdJECG
35 | artifact: xnbcli-windows-%PLATFORM%.zip
36 | on:
37 | branch: master
38 | appveyor_repo_tag: true
39 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 | os:
5 | - linux
6 | - osx
7 | install:
8 | - npm install
9 | - npm install pkg
10 | - ./.travis.build.sh
11 | deploy:
12 | skip_cleanup: true
13 | provider: releases
14 | api_key:
15 | secure: aK9vbh+idJX3fj9y7EeBrW1TKeiRfAcvPDRuZjdppJqVpcmpCgTf6T6a6+vYRDfr0r7VIDuqik2jyg7hOrExWBOv7ovkC9yAcCYSREcg0jZO9dK9g0c1VcaPIW4OqtArK+gTW20jbwxTSHGfphpjw9yNDRxUq87Qx+ciTQsF+Usg8QwmQ+Du7vXUo5WMKmWpVHhupcM7HT2IXDQ4OHqCwo0FRyBimRBS2gdS/qZmqya3QUMwyjFImMIphwOumsqckoS0PRJAEDJeKWkZeGXCwpPmZ8c3uliXwhaxEwQuzUKspMG2ZMJUY9OwcY3KYGdYGeNNBm6rf/ltglebWH0X+VjfRKmc5yftkiT0re3Og4l4TNSaijMqjseFbPyyJQDwMPJ7VmkX/fzUOEO7chzHoDcECiUi+LbF9KjOhajPuDRHaMYD2CkihQ2970QbYKsOXDd+hvTx8mAPmFMFEoFZiLXJsX+nvAw5nwxWdI5P5BsaKgogop7o8mYBUHAgzAslPJGL1uTSraSqmcaDtBJCea++6yOEdIFFrLmAjkgh1k1y9rUC/QV8uOGneLpvHnnMusPbGPHIwJPKs3qf/7wLHZnQs3xtTEV60xJfY3gmNTQ2gzL67nV6ih2Z1+dHgkMgF4JXRJiyTRvl47NKGaNXdi5/GtkBDTgrU1pDyupafQk=
16 | file:
17 | - "xnbcli-linux.zip"
18 | - "xnbcli-macos.zip"
19 | on:
20 | repo: LeonBlade/xnbcli
21 | draft: true
22 | tags: true
23 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/TBinReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWrite = require('../../BufferWriter');
4 | const Int32Reader = require('./Int32Reader');
5 |
6 | /**
7 | * TBin Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class TBinReader extends BaseReader {
12 | read(buffer) {
13 | const int32Reader = new Int32Reader();
14 |
15 | // read in the size of the data block
16 | let size = int32Reader.read(buffer);
17 | // read in the data block
18 | let data = buffer.read(size);
19 |
20 | // return the data
21 | return { export: { type: this.type, data } };
22 | }
23 |
24 | write(buffer, content, resolver) {
25 | this.writeIndex(buffer, resolver);
26 | const int32Reader = new Int32Reader();
27 | int32Reader.write(buffer, content.data.length, null);
28 | buffer.concat(content.data);
29 | }
30 |
31 | isValueType() {
32 | return false;
33 | }
34 | }
35 |
36 | module.exports = TBinReader;
37 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/Vector3Reader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const SingleReader = require('./SingleReader');
4 |
5 | /**
6 | * Vector3 Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class Vector3Reader extends BaseReader {
11 | /**
12 | * Reads Vector3 from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {object}
15 | */
16 | read(buffer) {
17 | const singleReader = new SingleReader();
18 |
19 | let x = singleReader.read(buffer);
20 | let y = singleReader.read(buffer);
21 | let z = singleReader.read(buffer);
22 |
23 | return { x, y, z };
24 | }
25 |
26 | write(buffer, content, resolver) {
27 | this.writeIndex(buffer, resolver);
28 | const singleReader = new SingleReader();
29 | singleReader.write(buffer, content.x, null);
30 | singleReader.write(buffer, content.y, null);
31 | singleReader.write(buffer, content.z, null);
32 | }
33 | }
34 |
35 | module.exports = Vector3Reader;
36 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/EffectReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const UInt32Reader = require('./UInt32Reader');
5 |
6 | /**
7 | * Effect Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class EffectReader extends BaseReader {
12 |
13 | read(buffer) {
14 | const uint32Reader = new UInt32Reader();
15 |
16 | const size = uint32Reader.read(buffer);
17 | const bytecode = buffer.read(size);
18 |
19 | return { export: { type: this.type, data: bytecode } };
20 | }
21 |
22 | /**
23 | * Writes Effects into the buffer
24 | * @param {BufferWriter} buffer
25 | * @param {Mixed} data The data
26 | * @param {ReaderResolver} resolver
27 | */
28 | write(buffer, content, resolver) {
29 | this.writeIndex(buffer, resolver);
30 |
31 | const uint32Reader = new UInt32Reader();
32 |
33 | uint32Reader.write(buffer, content.data.length, null);
34 | buffer.concat(content.data);
35 | }
36 |
37 | isValueType() {
38 | return false;
39 | }
40 | }
41 |
42 | module.exports = EffectReader;
43 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/BmFontReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const StringReader = require('./StringReader');
5 | const XnbError = require('../../XnbError');
6 | const fs = require('fs');
7 |
8 | /**
9 | * BmFont Reader
10 | * @class
11 | * @extends BaseReader
12 | */
13 | class BmFontReader extends BaseReader {
14 | /**
15 | * Reads BmFont from buffer.
16 | * @param {BufferReader} buffer
17 | * @returns {Object}
18 | */
19 | read(buffer) {
20 | const stringReader = new StringReader();
21 | const xml = stringReader.read(buffer);
22 | return { export: { type: this.type, data: xml } };
23 | }
24 |
25 | /**
26 | * Writes BmFont into buffer
27 | * @param {BufferWriter} buffer
28 | * @param {Mixed} data
29 | * @param {ReaderResolver}
30 | */
31 | write(buffer, content, resolver) {
32 | // write index of reader
33 | this.writeIndex(buffer, resolver);
34 | const stringReader = new StringReader();
35 | stringReader.write(buffer, content.data, null);
36 | }
37 |
38 | isValueType() {
39 | return false;
40 | }
41 | }
42 |
43 | module.exports = BmFontReader;
44 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/CharReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const XnbError = require('../../XnbError');
5 |
6 | /**
7 | * Char Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class CharReader extends BaseReader {
12 | /**
13 | * Reads Char from the buffer.
14 | * @param {BufferReader} buffer
15 | * @returns {String}
16 | */
17 | read(buffer) {
18 | let charSize = this._getCharSize(buffer.peek(1).readInt8());
19 | return buffer.read(charSize).toString('utf8');
20 | }
21 |
22 | /**
23 | * Writes Char into buffer
24 | * @param {BufferWriter} buffer
25 | * @param {Mixed} data
26 | * @param {ReaderResolver}
27 | */
28 | write(buffer, content, resolver) {
29 | this.writeIndex(buffer, resolver);
30 | buffer.write(content);
31 | }
32 |
33 | /**
34 | * Gets size of char for some special characters that are more than one byte.
35 | * @param {Number} byte
36 | * @returns {Number}
37 | */
38 | _getCharSize(byte) {
39 | return (( 0xE5000000 >> (( byte >> 3 ) & 0x1e )) & 3 ) + 1;
40 | }
41 | }
42 |
43 | module.exports = CharReader;
44 |
--------------------------------------------------------------------------------
/app/Xact/XactClip.js:
--------------------------------------------------------------------------------
1 | const Log = require('../Log');
2 | const BufferReader = require('../BufferReader');
3 |
4 | class XactClip {
5 | constructor(buffer, clipOffset) {
6 | const oldPosition = buffer.bytePosition;
7 | buffer.seek(clipOffset, 0);
8 |
9 | //buffer.debug();
10 | const numEvents = buffer.readByte();
11 | const events = new Array(numEvents);
12 |
13 | for (let i = 0; i < numEvents; i++) {
14 | const eventInfo = buffer.readUInt32();
15 | const eventId = eventInfo & 0x1F;
16 | switch (eventId) {
17 | case 1:
18 | const evnt = {};
19 | const trackIndex = buffer.readUInt16();
20 | const waveBankIndex = buffer.readByte();
21 |
22 | buffer.seek(5);
23 |
24 | Log.debug(`Track: ${Log.h(trackIndex)}`);
25 | break;
26 | case 4:
27 | Log.debug(`EventPlayWavePitchVolumeFilterVariation`);
28 | break;
29 | default:
30 | Log.warn(`Event id ${eventId} not implemented`);
31 | break;
32 | }
33 | }
34 | buffer.seek(oldPosition, 0);
35 | }
36 | }
37 |
38 | module.exports = XactClip;
39 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/RectangleReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const Int32Reader = require('./Int32Reader');
4 |
5 | /**
6 | * Rectangle Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class RectangleReader extends BaseReader {
11 | /**
12 | * Reads Rectangle from buffer.
13 | * @param {BufferReader} buffer
14 | * @returns {object}
15 | */
16 | read(buffer) {
17 | const int32Reader = new Int32Reader();
18 |
19 | const x = int32Reader.read(buffer);
20 | const y = int32Reader.read(buffer);
21 | const width = int32Reader.read(buffer);
22 | const height = int32Reader.read(buffer);
23 |
24 | return { x, y, width, height };
25 | }
26 |
27 | /**
28 | * Writes Effects into the buffer
29 | * @param {BufferWriter} buffer
30 | * @param {Mixed} data The data
31 | * @param {ReaderResolver} resolver
32 | */
33 | write(buffer, content, resolver) {
34 | this.writeIndex(buffer, resolver);
35 | const int32Reader = new Int32Reader();
36 | int32Reader.write(buffer, content.x, null);
37 | int32Reader.write(buffer, content.y, null);
38 | int32Reader.write(buffer, content.width, null);
39 | int32Reader.write(buffer, content.height, null);
40 | }
41 | }
42 |
43 | module.exports = RectangleReader;
44 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/StringReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const ReaderResolver = require('../ReaderResolver');
5 |
6 | /**
7 | * String Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class StringReader extends BaseReader {
12 | /**
13 | * Reads String from buffer.
14 | * @param {BufferReader} buffer
15 | * @returns {String}
16 | */
17 | read(buffer) {
18 | // read in the length of the string
19 | let length = buffer.read7BitNumber();
20 | // read in the UTF-8 encoded string
21 | return buffer.read(length).toString('utf8');
22 | }
23 |
24 | /**
25 | * Writes the string to the buffer.
26 | * @param {BufferWriter} buffer
27 | * @param {String} string
28 | * @param {ReaderResolver} resolver
29 | */
30 | write(buffer, string, resolver) {
31 | // write the index
32 | this.writeIndex(buffer, resolver);
33 | // get the size of the string
34 | const size = Buffer.byteLength(string);
35 | // write the length of the string
36 | buffer.write7BitNumber(size);
37 | // write the string
38 | buffer.write(string, size);
39 | }
40 |
41 | isValueType() {
42 | return false;
43 | }
44 | }
45 |
46 | module.exports = StringReader;
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xnbcli
2 |
3 | [](https://travis-ci.org/LeonBlade/xnbcli)
4 |
5 | A CLI tool for XNB packing/unpacking purpose built for Stardew Valley.
6 |
7 | This tool currently supports unpacking all LZX compressed XNB files for Stardew Valley.
8 | There is some basic groundwork for XACT as well.
9 |
10 | The end goal for this project is to serve as a CLI for a GUI wrapper to leverage so the average user can interface with
11 | XNB files a lot easier.
12 |
13 | ## Usage
14 |
15 | **Unpacking XNB files**
16 |
17 | Place any files you wish to extract in the `packed` folder and run the appropriate file for unpacking. `unpack.bat`, `unpack.command` or `unpack.sh`.
18 |
19 | **Packing XNB files**
20 |
21 | Place any files you wish to repack back into XNB files in the `unpacked` folder and run the appropriate file for packing. `pack.bat`, `pack.command` or `pack.sh`.
22 |
23 | **Terminal Use**
24 |
25 | `xnbcli (pack|unpack) [input] [output]`
26 |
27 | ## Developers
28 |
29 | If you wish to run this with Node.js and all the source code, please refer to the following.
30 |
31 | - `node.js` installed
32 | - `npm` installed
33 | - `python` installed
34 | - (for Windows users) `windows-build-tools` installed (`npm i --g --production windows-build-tools`)
35 | - Run `npm install` to install node packages.
36 | - `npm run unpack` and `npm run pack` scripts are available for use in the `package.json` file.
37 |
38 | ## License
39 | GNU LGPL v3.0
40 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/ListReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const ReaderResolver = require('../ReaderResolver');
4 | const UInt32Reader = require('./UInt32Reader');
5 |
6 | /**
7 | * List Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class ListReader extends BaseReader {
12 | constructor(reader) {
13 | super();
14 | /** @type {BaseReader} */
15 | this.reader = reader;
16 | }
17 |
18 | /**
19 | * Reads List from buffer.
20 | * @param {BufferReader} buffer
21 | * @param {ReaderResolver} resolver
22 | * @returns {Array}
23 | */
24 | read(buffer, resolver) {
25 | const uint32Reader = new UInt32Reader();
26 | const size = uint32Reader.read(buffer);
27 |
28 | const list = [];
29 | for (let i = 0; i < size; i++) {
30 | const value = this.reader.isValueType() ? this.reader.read(buffer) : resolver.read(buffer);
31 | list.push(value);
32 | }
33 | return list;
34 | }
35 |
36 | /**
37 | * Writes List into the buffer
38 | * @param {BufferWriter} buffer
39 | * @param {Mixed} data The data
40 | * @param {ReaderResolver} resolver
41 | */
42 | write(buffer, content, resolver) {
43 | this.writeIndex(buffer, resolver);
44 | const uint32Reader = new UInt32Reader();
45 | uint32Reader.write(buffer, content.length, null);
46 | for (let i in content)
47 | this.reader.write(buffer, content[i], (this.reader.isValueType ? null : resolver));
48 | }
49 |
50 | get type() {
51 | return `List<${this.reader.type}>`;
52 | }
53 | }
54 |
55 | module.exports = ListReader;
56 |
--------------------------------------------------------------------------------
/app/Xnb/ReaderResolver.js:
--------------------------------------------------------------------------------
1 | const TypeReader = require('./TypeReader');
2 | const Log = require('../Log');
3 | const BufferReader = require('../BufferReader');
4 | const XnbError = require('../XnbError');
5 |
6 | /**
7 | * Class used to read the XNB types using the readers
8 | * @class
9 | */
10 | class ReaderResolver {
11 | /**
12 | * Creating a new instance of ReaderResolver
13 | * @constructor
14 | * @param {BaseReader[]} readers Array of BaseReaders
15 | */
16 | constructor(readers) {
17 | /**
18 | * Array of base readers
19 | * @type {BaseReader[]}
20 | */
21 | this.readers = readers;
22 | }
23 |
24 | /**
25 | * Read the XNB file contents
26 | * @method read
27 | * @public
28 | * @param {BufferReader} buffer The buffer to read from.
29 | */
30 | read(buffer) {
31 | // read the index of which reader to use
32 | let index = buffer.read7BitNumber() - 1;
33 | if (this.readers[index] == null)
34 | throw new XnbError(`Invalid reader index ${index}`);
35 | // read the buffer using the selected reader
36 | return this.readers[index].read(buffer, this);
37 | }
38 |
39 | /**
40 | * Writes the XNB file contents
41 | * @param {BufferWriter} buffer
42 | * @param {Object} content
43 | */
44 | write(buffer, content) {
45 | this.readers[0].write(buffer, content, this);
46 | }
47 |
48 | /**
49 | * Returns the index of the reader
50 | * @param {BaseReader} reader
51 | * @param {Number}
52 | */
53 | getIndex(reader) {
54 | for (let i in this.readers)
55 | if (reader.toString() == this.readers[i].toString())
56 | return i;
57 | }
58 | }
59 |
60 | module.exports = ReaderResolver;
61 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/index.js:
--------------------------------------------------------------------------------
1 | // list of readers available
2 | const readers = [
3 | 'ArrayReader',
4 | 'BaseReader',
5 | 'BmFontReader',
6 | 'BooleanReader',
7 | 'CharReader',
8 | 'DictionaryReader',
9 | 'DoubleReader',
10 | 'EffectReader',
11 | 'Int32Reader',
12 | 'ListReader',
13 | 'NullableReader',
14 | 'RectangleReader',
15 | 'SingleReader',
16 | 'SoundEffectReader',
17 | 'SpriteFontReader',
18 | 'StringReader',
19 | 'TBinReader',
20 | 'Texture2DReader',
21 | 'UInt32Reader',
22 | 'Vector2Reader',
23 | 'Vector3Reader',
24 | 'Vector4Reader'
25 | ];
26 |
27 | // loop over readers to export them
28 | //for (let reader of readers)
29 | // exports[reader] = require(`./${reader}`);
30 |
31 | module.exports = {
32 | ArrayReader: require('./ArrayReader'),
33 | BaseReader: require('./BaseReader'),
34 | BmFontReader: require('./BmFontReader'),
35 | BooleanReader: require('./BooleanReader'),
36 | CharReader: require('./CharReader'),
37 | DictionaryReader: require('./DictionaryReader'),
38 | DoubleReader: require('./DoubleReader'),
39 | EffectReader: require('./EffectReader'),
40 | Int32Reader: require('./Int32Reader'),
41 | ListReader: require('./ListReader'),
42 | NullableReader: require('./NullableReader'),
43 | RectangleReader: require('./RectangleReader'),
44 | SingleReader: require('./SingleReader'),
45 | SoundEffectReader: require('./SoundEffectReader'),
46 | SpriteFontReader: require('./SpriteFontReader'),
47 | StringReader: require('./StringReader'),
48 | TBinReader: require('./TBinReader'),
49 | Texture2DReader: require('./Texture2DReader'),
50 | UInt32Reader: require('./UInt32Reader'),
51 | Vector2Reader: require('./Vector2Reader'),
52 | Vector3Reader: require('./Vector3Reader'),
53 | Vector4Reader: require('./Vector4Reader')
54 | };
55 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/SoundEffectReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const XnbError = require('../../XnbError');
5 |
6 | /**
7 | * Single Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class SoundEffectReader extends BaseReader {
12 | /**
13 | * Reads Single from the buffer.
14 | * @param {BufferReader} buffer
15 | * @returns {Number}
16 | */
17 | read(buffer) {
18 | const RIFF = 'RIFF';
19 | const WAVE = 'WAVE';
20 | const fmt = 'fmt ';
21 | const data = 'data';
22 | const fmtHeaderSize = 20; // size in bytes of the WAVE file header (this will always be the same given these constants ^)
23 | const dataHeaderSize = 8;
24 |
25 | // Read the fmt and data sections
26 | const fmtLength = buffer.readUInt32();
27 | const fmtBuffer = buffer.read(fmtLength);
28 | const dataLength = buffer.readUInt32();
29 | const dataBuffer = buffer.read(dataLength);
30 |
31 | // Build a header for the fmt section
32 | let fmtHeader = new BufferWriter(fmtHeaderSize);
33 | fmtHeader.write(RIFF);
34 | fmtHeader.writeUInt32(fmtHeaderSize + fmtLength + dataLength);
35 | fmtHeader.write(WAVE);
36 | fmtHeader.write(fmt);
37 | fmtHeader.writeUInt32(fmtLength);
38 |
39 | // Build a header for the data section
40 | let dataHeader = new BufferWriter(dataHeaderSize);
41 | dataHeader.write(data);
42 | dataHeader.writeUInt32(dataLength);
43 |
44 | // And put them all together
45 | let finalBuffer = Buffer.concat([fmtHeader.buffer, fmtBuffer, dataHeader.buffer, dataBuffer]);
46 | return { export: {type: this.type, data: finalBuffer} }
47 | }
48 | }
49 |
50 | module.exports = SoundEffectReader;
51 |
--------------------------------------------------------------------------------
/app/Xact/XactSound.js:
--------------------------------------------------------------------------------
1 | const Log = require('../Log');
2 | const BufferReader = require('../BufferReader');
3 | const XactClip = require('./XactClip');
4 |
5 | class XactSound {
6 | constructor(buffer, soundOffset) {
7 | const oldPosition = buffer.bytePosition;
8 | buffer.seek(soundOffset, 0);
9 |
10 | const flags = buffer.readByte();
11 | const complexSound = (flags & 1) != 0;
12 |
13 | const category = buffer.readUInt16();
14 | buffer.seek(1);
15 | const volume = buffer.readUInt16();
16 | buffer.seek(1);
17 | const entryLength = buffer.readUInt16();
18 |
19 | let numClips = 0;
20 | if (complexSound) {
21 | numClips = buffer.readByte();
22 | }
23 | else {
24 | const trackIndex = buffer.readUInt16();
25 | const waveBankIndex = buffer.readByte();
26 | // wave =
27 | Log.debug(`Track Index: ${Log.h(trackIndex)}`);
28 | Log.debug(`WaveBank Index: ${(waveBankIndex)}`);
29 | }
30 |
31 | if ((flags & 0x1E) != 0) {
32 | const extraDataLen = buffer.readUInt16();
33 |
34 | if (complexSound) Log.info(`Extra Data Length: ${extraDataLen}`);
35 | // TODO: parse RPC+DSP stuff
36 | buffer.seek(extraDataLen);
37 | }
38 |
39 | if (complexSound) {
40 | for (let i = 0; i < numClips; i++) {
41 | buffer.seek(1);
42 | const clipOffset = buffer.readUInt32();
43 | Log.debug(`Offset: ${clipOffset}`);
44 | try {
45 | buffer.seek(4);
46 | const clip = new XactClip(buffer, clipOffset);
47 | }
48 | catch (ex) {
49 | Log.warn(`Offset too large!`);
50 | }
51 | }
52 | }
53 |
54 | buffer.seek(oldPosition, 0);
55 | }
56 | }
57 |
58 | module.exports = XactSound;
59 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/NullableReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const BooleanReader = require('./BooleanReader');
5 |
6 | /**
7 | * Nullable Reader
8 | * @class
9 | * @extends BaseReader
10 | */
11 | class NullableReader extends BaseReader {
12 | /**
13 | * @constructor
14 | * @param {BaseReader} reader
15 | */
16 | constructor(reader) {
17 | super();
18 | /**
19 | * Nullable type
20 | * @type {BaseReader}
21 | */
22 | this.reader = reader;
23 | }
24 |
25 | /**
26 | * Reads Nullable type from buffer.
27 | * @param {BufferReader} buffer
28 | * @param {ReaderResolver} resolver
29 | * @returns {mixed|null}
30 | */
31 | read(buffer, resolver) {
32 | // get an instance of boolean reader
33 | const booleanReader = new BooleanReader();
34 | // read in if the nullable has a value or not
35 | const hasValue = booleanReader.read(buffer);
36 |
37 | // return the value
38 | return (hasValue ? (this.reader.isValueType() ? this.reader.read(buffer) : resolver.read(buffer)) : null);
39 | }
40 |
41 | /**
42 | * Writes Nullable into the buffer
43 | * @param {BufferWriter} buffer
44 | * @param {Mixed} data The data
45 | * @param {ReaderResolver} resolver
46 | */
47 | write(buffer, content, resolver) {
48 | //this.writeIndex(buffer, resolver);
49 | const booleanReader = new BooleanReader();
50 | buffer.writeByte(content != null);
51 | if (content != null)
52 | this.reader.write(buffer, content, (this.reader.isValueType() ? null : resolver));
53 | }
54 |
55 | isValueType() {
56 | return false;
57 | }
58 |
59 | get type() {
60 | return `Nullable<${this.reader.type}>`;
61 | }
62 | }
63 |
64 | module.exports = NullableReader;
65 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/BaseReader.js:
--------------------------------------------------------------------------------
1 | const XnbError = require('../../XnbError');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const ReaderResolver = require('../ReaderResolver');
5 |
6 | /**
7 | * Base class for all readers.
8 | * @abstract
9 | * @class
10 | */
11 | class BaseReader {
12 | /**
13 | * Returns if type normally requires a special reader.
14 | * @public
15 | * @method
16 | * @returns {Boolean} Returns true if type is primitive.
17 | */
18 | isValueType() {
19 | return true;
20 | }
21 |
22 | /**
23 | * Returns string type of reader
24 | * @public
25 | * @property
26 | * @returns {string}
27 | */
28 | get type() {
29 | return this.constructor.name.slice(0, -6);
30 | }
31 |
32 | /**
33 | * Reads the buffer by the specification of the type reader.
34 | * @public
35 | * @param {BufferReader} buffer The buffer to read from.
36 | * @param {ReaderResolver} resolver The content reader to resolve readers from.
37 | * @returns {mixed} Returns the type as specified by the type reader.
38 | */
39 | read(buffer, resolver) {
40 | throw new XnbError('Cannot invoke methods on abstract class.');
41 | }
42 |
43 | /**
44 | * Writes into the buffer
45 | * @param {BufferWriter} buffer The buffer to write to
46 | * @param {Mixed} data The data to parse to write to the buffer
47 | * @param {ReaderResolver} resolver ReaderResolver to write non-primitive types
48 | */
49 | write(buffer, content, resolver) {
50 | throw new XnbError('Cannot invoke methods on abstract class.');
51 | }
52 |
53 | /**
54 | * Writes the index of this reader to the buffer
55 | * @param {BufferWriter} buffer
56 | * @param {ReaderResolver} resolver
57 | */
58 | writeIndex(buffer, resolver) {
59 | if (resolver != null)
60 | buffer.write7BitNumber(Number.parseInt(resolver.getIndex(this)) + 1);
61 | }
62 |
63 | /**
64 | * When printing out in a string.
65 | * @returns {String}
66 | */
67 | toString() {
68 | return this.type;
69 | }
70 | }
71 |
72 | module.exports = BaseReader;
73 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/ArrayReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const UInt32Reader = require('./UInt32Reader');
4 |
5 | /**
6 | * Array Reader
7 | * @class
8 | * @extends BaseReader
9 | */
10 | class ArrayReader extends BaseReader {
11 |
12 | /**
13 | * Constructor for the ArrayReader
14 | * @param {BaseReader} reader The reader used for the array elements
15 | */
16 | constructor(reader) {
17 | super();
18 | /** @type {BaseReader} */
19 | this.reader = reader;
20 | }
21 |
22 | /**
23 | * Reads Array from buffer.
24 | * @param {BufferReader} buffer
25 | * @param {ReaderResolver} resolver
26 | * @returns {Array}
27 | */
28 | read(buffer, resolver) {
29 | // create a uint32 reader
30 | const uint32Reader = new UInt32Reader();
31 | // read the number of elements in the array
32 | let size = uint32Reader.read(buffer);
33 | // create local array
34 | let array = [];
35 |
36 | // loop size number of times for the array elements
37 | for (let i = 0; i < size; i++) {
38 | // get value from buffer
39 | let value = this.reader.isValueType() ? this.reader.read(buffer) : resolver.read(buffer);
40 | // push into local array
41 | array.push(value);
42 | }
43 |
44 | // return the array
45 | return array;
46 | }
47 |
48 | /**
49 | * Writes Array into buffer
50 | * @param {BufferWriter} buffer
51 | * @param {Array} data
52 | * @param {ReaderResolver} resolver
53 | */
54 | write(buffer, content, resolver) {
55 | // write the index
56 | this.writeIndex(buffer, resolver);
57 | // create a uint32 reader
58 | const uint32Reader = new UInt32Reader();
59 | // write the number of elements in the array
60 | uint32Reader.write(buffer, content.length, resolver);
61 |
62 | // loop over array to write array contents
63 | for (let i of content)
64 | this.reader.write(buffer, content[i], (this.reader.isValueType() ? null : resolver));
65 | }
66 |
67 | isValueType() {
68 | return false;
69 | }
70 |
71 | get type() {
72 | return `Array<${this.reader.type}>`;
73 | }
74 | }
75 |
76 | module.exports = ArrayReader;
77 |
--------------------------------------------------------------------------------
/app/Presser/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const Lzx = require('./Lzx');
3 | const BufferReader = require('../BufferReader');
4 | const XnbError = require('../XnbError');
5 | const Log = require('../Log');
6 |
7 | /**
8 | * Used to compress and decompress LZX.
9 | * @class
10 | * @public
11 | */
12 | class Presser {
13 |
14 | /**
15 | * Decompress a certain amount of bytes.
16 | * @public
17 | * @static
18 | * @param {BufferReader} buffer
19 | * @returns {Buffer}
20 | */
21 | static decompress(buffer, compressedTodo, decompressedTodo) {
22 | // current index into the buffer
23 | let pos = 0;
24 |
25 | // allocate variables for block and frame size
26 | let block_size;
27 | let frame_size;
28 |
29 | // create the LZX instance with 16-bit window frame
30 | const lzx = new Lzx(16);
31 |
32 | // the full decompressed array
33 | let decompressed = [];
34 | let z = 0;
35 |
36 | // loop over the bytes left
37 | while (pos < compressedTodo) {
38 | // flag is for determining if frame_size is fixed or not
39 | const flag = buffer.readByte();
40 |
41 | // if flag is set to 0xFF that means we will read in frame size
42 | if (flag == 0xFF) {
43 | // read in the frame size
44 | frame_size = buffer.readLZXInt16();
45 | // read in the block size
46 | block_size = buffer.readLZXInt16();
47 | // advance the byte position forward
48 | pos += 5;
49 | }
50 | else {
51 | // rewind the buffer
52 | buffer.seek(-1);
53 | // read in the block size
54 | block_size = buffer.readLZXInt16(this.buffer);
55 | // set the frame size
56 | frame_size = 0x8000;
57 | // advance byte position forward
58 | pos += 2;
59 | }
60 |
61 | // ensure the block and frame size aren't empty
62 | if (block_size == 0 || frame_size == 0)
63 | break;
64 |
65 | // ensure the block and frame size don't exceed size of integers
66 | if (block_size > 0x10000 || frame_size > 0x10000)
67 | throw new XnbError('Invalid size read in compression content.');
68 |
69 | Log.debug(`Block Size: ${block_size}, Frame Size: ${frame_size}`);
70 |
71 | // decompress the file based on frame and block size
72 | decompressed = decompressed.concat(lzx.decompress(buffer, frame_size, block_size));
73 |
74 | // increase position counter
75 | pos += block_size;
76 | }
77 |
78 | // we have finished decompressing the file
79 | Log.info('File has been successfully decompressed!');
80 |
81 | // return a decompressed buffer
82 | return Buffer.from(decompressed);
83 | }
84 | }
85 |
86 | module.exports = Presser;
87 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/SpriteFontReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const Int32Reader = require('./Int32Reader');
4 | const SingleReader = require('./SingleReader');
5 | const NullableReader = require('./NullableReader');
6 | const CharReader = require('./CharReader');
7 | const Texture2DReader = require('./Texture2DReader');
8 | const ListReader = require('./ListReader');
9 | const RectangleReader = require('./RectangleReader');
10 | const Vector3Reader = require('./Vector3Reader');
11 |
12 | /**
13 | * SpriteFont Reader
14 | * @class
15 | * @extends BaseReader
16 | */
17 | class SpriteFontReader extends BaseReader {
18 | /**
19 | * Reads SpriteFont from buffer.
20 | * @param {BufferReader} buffer
21 | * @param {ReaderResolver} resolver
22 | * @returns {object}
23 | */
24 | read(buffer, resolver) {
25 | const int32Reader = new Int32Reader();
26 | const singleReader = new SingleReader();
27 | const nullableCharReader = new NullableReader(new CharReader());
28 |
29 | const texture = resolver.read(buffer)
30 | const glyphs = resolver.read(buffer);
31 | const cropping = resolver.read(buffer);;
32 | const characterMap = resolver.read(buffer);
33 | const verticalLineSpacing = int32Reader.read(buffer);
34 | const horizontalSpacing = singleReader.read(buffer);
35 | const kerning = resolver.read(buffer);
36 | const defaultCharacter = nullableCharReader.read(buffer);
37 |
38 | return {
39 | texture,
40 | glyphs,
41 | cropping,
42 | characterMap,
43 | verticalLineSpacing,
44 | horizontalSpacing,
45 | kerning,
46 | defaultCharacter
47 | };
48 | }
49 |
50 | write(buffer, content, resolver) {
51 | const int32Reader = new Int32Reader();
52 | const charReader = new CharReader();
53 | const singleReader = new SingleReader();
54 | const nullableCharReader = new NullableReader(charReader);
55 | const texture2DReader = new Texture2DReader();
56 | const rectangleListReader = new ListReader(new RectangleReader());
57 | const charListReader = new ListReader(charReader);
58 | const vector3ListReader = new ListReader(new Vector3Reader());
59 |
60 | this.writeIndex(buffer, resolver);
61 |
62 | try {
63 | texture2DReader.write(buffer, content.texture, resolver);
64 | rectangleListReader.write(buffer, content.glyphs, resolver);
65 | rectangleListReader.write(buffer, content.cropping, resolver);
66 | charListReader.write(buffer, content.characterMap, resolver);
67 | int32Reader.write(buffer, content.verticalLineSpacing, null);
68 | singleReader.write(buffer, content.horizontalSpacing, null);
69 | vector3ListReader.write(buffer, content.kerning, resolver);
70 | nullableCharReader.write(buffer, content.defaultCharacter, null);
71 | }
72 | catch (ex) {
73 | throw ex;
74 | }
75 | }
76 |
77 | isValueType() {
78 | return false;
79 | }
80 | }
81 |
82 | module.exports = SpriteFontReader;
83 |
--------------------------------------------------------------------------------
/app/Log.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | const LOG_DEBUG = 0b0001;
4 | const LOG_INFO = 0b0010;
5 | const LOG_WARN = 0b0100;
6 | const LOG_ERROR = 0b1000;
7 |
8 | let _info = true, _warn = true, _error = true, _debug = false;
9 |
10 | /**
11 | * Log class with static members to log messages to the console.
12 | * @class
13 | * @static
14 | */
15 | class Log {
16 |
17 | static get DEBUG() { return LOG_DEBUG; }
18 | static get INFO() { return LOG_INFO; }
19 | static get WARN() { return LOG_WARN; }
20 | static get ERROR() { return LOG_ERROR; }
21 |
22 | /**
23 | * Sets the debug mode setting.
24 | * @public
25 | * @method setMode
26 | * @param {Number} log
27 | * @param {Boolean} state
28 | */
29 | static setMode(log, state) {
30 | if (log & LOG_DEBUG)
31 | _debug = state;
32 | if (log & LOG_INFO)
33 | _info = state;
34 | if (log & LOG_WARN)
35 | _warn = state;
36 | if (log & LOG_ERROR)
37 | _error = state;
38 | }
39 |
40 | /**
41 | * Displays an info message
42 | * @param {String} message Message to display to the console as info.
43 | */
44 | static info(message = '') {
45 | if (_info)
46 | console.log(chalk.bold.blue('[INFO] ') + message);
47 | }
48 |
49 | /**
50 | * Displays a debug message
51 | * @param {String} message Message to display to the console if debug is enabled.
52 | */
53 | static debug(message = '') {
54 | if (_debug)
55 | console.log(chalk.bold.magenta('[DEBUG] ') + message);
56 | }
57 |
58 | /**
59 | * Displays a warning message
60 | * @param {String} message Message to display to the console as a warning.
61 | */
62 | static warn(message = '') {
63 | if (_warn)
64 | console.log(chalk.bold.yellow('[WARN] ') + message);
65 | }
66 |
67 | /**
68 | * Displays an error message
69 | * @param {String} message Message to display to the console as an error.
70 | */
71 | static error(message = '') {
72 | if (_error)
73 | console.log(chalk.bold.red('[ERROR] ') + message);
74 | }
75 |
76 | /**
77 | * Displays a binary message
78 | * @param {Number} n
79 | * @param {Number} size
80 | * @param {Number} sliceBegin
81 | * @param {Number} sliceEnd
82 | * @returns {String}
83 | */
84 | static b(n, size = 8, sliceBegin = -1, sliceEnd = -1) {
85 | var z = ''
86 | while (z.length < size)
87 | z += '0';
88 | z = z.slice(n.toString(2).length) + n.toString(2);
89 | if (sliceBegin == -1 && sliceEnd == -1)
90 | return `0b${z}`;
91 | return chalk.gray('0b') +
92 | chalk.gray(z.slice(0, sliceBegin)) +
93 | chalk.bold.blue('[') + chalk.bold(z.slice(sliceBegin, sliceEnd)) + chalk.bold.blue(']') +
94 | chalk.gray(z.slice(sliceEnd));
95 | }
96 |
97 | /**
98 | * Displays a hex message
99 | * @param {Number} n
100 | * @returns {String}
101 | */
102 | static h(n) {
103 | return `0x${n.toString(16)}`;
104 | }
105 | }
106 |
107 | // export the log
108 | module.exports = Log;
109 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/DictionaryReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const BufferWriter = require('../../BufferWriter');
4 | const ReaderResolver = require('../ReaderResolver');
5 | const UInt32Reader = require('./UInt32Reader');
6 |
7 | /**
8 | * Dictionary Reader
9 | * @class
10 | * @extends BaseReader
11 | */
12 | class DictionaryReader extends BaseReader {
13 |
14 | /**
15 | * Constructor for DictionaryReader.
16 | * @constructor
17 | * @param {BaseReader} key The BaseReader for the dictionary key.
18 | * @param {BaseReader} value The BaseReader for the dictionary value.
19 | */
20 | constructor(key, value) {
21 | // verify key and value are specified
22 | if (key == undefined || value == undefined)
23 | throw new XnbError('Cannot create instance of DictionaryReader without Key and Value.');
24 |
25 | // call base constructor
26 | super();
27 |
28 | /** @type {BaseReader} */
29 | this.key = key;
30 | /** @type {BaseReader} */
31 | this.value = value;
32 | }
33 |
34 | /**
35 | * Reads Dictionary from buffer.
36 | * @param {BufferReader} buffer Buffer to read from.
37 | * @param {ReaderResolver} resolver ReaderResolver to read non-primitive types.
38 | * @returns {object}
39 | */
40 | read(buffer, resolver) {
41 | // the dictionary to return
42 | let dictionary = {};
43 |
44 | // read in the size of the dictionary
45 | const uint32Reader = new UInt32Reader();
46 | const size = uint32Reader.read(buffer);
47 |
48 | // loop over the size of the dictionary and read in the data
49 | for (let i = 0; i < size; i++) {
50 | // get the key
51 | let key = this.key.isValueType() ? this.key.read(buffer) : resolver.read(buffer);
52 | // get the value
53 | let value = this.value.isValueType() ? this.value.read(buffer) : resolver.read(buffer);
54 |
55 | // assign KV pair to the dictionary
56 | dictionary[key] = value;
57 | }
58 |
59 | // return the dictionary object
60 | return dictionary;
61 | }
62 |
63 | /**
64 | * Writes Dictionary into buffer
65 | * @param {BufferWriter} buffer
66 | * @param {Object} data The data to parse for the
67 | * @param {ReaderResolver} resolver ReaderResolver to write non-primitive types
68 | * @returns {Buffer} buffer instance with the data in it
69 | */
70 | write(buffer, content, resolver) {
71 | // write the index
72 | this.writeIndex(buffer, resolver);
73 |
74 | // write the amount of entries in the Dictionary
75 | buffer.writeUInt32(Object.keys(content).length);
76 |
77 | // loop over the entries
78 | for (let key of Object.keys(content)) {
79 | // write the key
80 | this.key.write(buffer, key, (this.key.isValueType() ? null : resolver));
81 | // write the value
82 | this.value.write(buffer, content[key], (this.value.isValueType() ? null : resolver));
83 | }
84 | }
85 |
86 | isValueType() {
87 | return false;
88 | }
89 |
90 | get type() {
91 | return `Dictionary<${this.key.type},${this.value.type}>`;
92 | }
93 | }
94 |
95 | module.exports = DictionaryReader;
96 |
--------------------------------------------------------------------------------
/app/Xact/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const Log = require('../Log');
3 | const XnbError = require('../XnbError');
4 |
5 | const AudioEngine = require('./AudioEngine');
6 | const SoundBank = require('./SoundBank');
7 | const WaveBank = require('./WaveBank');
8 |
9 | // WaveBank Constants
10 | const WBND_ENTRY_NAMES = 0x00010000; // bank includes entry names
11 | const WBND_COMPACT = 0x00020000; // bank uses compact format
12 | const WBND_SYNC_DISABLED = 0x00040000; // bank is disabled for audition sync
13 | const WBND_SEEK_TABLES = 0x00080000; // bank includes seek tables
14 | const WBND_MASK = 0x000F0000;
15 |
16 | /**
17 | * Used to pack and unpack xact files
18 | * @class
19 | * @public
20 | */
21 | class Xact {
22 |
23 | /**
24 | * Used to load a specific file.
25 | * @public
26 | * @static
27 | * @param {String} filename
28 | */
29 | static load(filename) {
30 | // get the extention name from the file
31 | const ext = path.extname(filename).toLowerCase().slice(1);
32 |
33 | // check our valid files
34 | switch (ext) {
35 | // AudioEngine
36 | case 'xgs':
37 | // create instance of the audio engine
38 | const audioEngine = new AudioEngine();
39 | // load the audio engine file
40 | audioEngine.load(filename);
41 | break;
42 | case 'xsb':
43 | SoundBank.load(filename);
44 | break;
45 | default:
46 | throw new XnbError(`Invalid file!`);
47 | }
48 | }
49 |
50 | }
51 |
52 | class Variable {
53 | constructor() {
54 | this.name = '';
55 | this.value = 0.0;
56 |
57 | this.isGlobal = false;
58 | this.isReadOnly = false;
59 | this.isPublic = false;
60 | this.isReserved = false;
61 |
62 | this.initValue = 0.0;
63 | this.maxValue = 0.0;
64 | this.minValue = 0.0;
65 | }
66 | }
67 |
68 | const RpcPointType = {
69 | Linear: 0,
70 | Fast: 1,
71 | Slow: 2,
72 | SinCos: 3
73 | };
74 |
75 | class RpcPoint {
76 | constructor() {
77 | this.x = 0.0;
78 | this.y = 0.0;
79 | this.type = 0;
80 | }
81 | }
82 |
83 | const RpcParmeter = {
84 | Volume: 0,
85 | Pitch: 1,
86 | ReverbSend: 2,
87 | FilterFrequency: 3,
88 | FilterQFactor: 4
89 | };
90 |
91 | class RpcCurve {
92 | constructor() {
93 | this.variable = 0;
94 | this.parameter = 0;
95 | this.points = [];
96 | }
97 | }
98 |
99 | class Segment {
100 | constructor() {
101 | this.offset = 0;
102 | this.length = 0;
103 | }
104 | }
105 |
106 | class WaveBankEntry {
107 | constructor() {
108 | this.format = 0;
109 | this.playRegion = new Segment();
110 | this.loopRegion = new Segment();
111 | this.flagsAndDuration = 0;
112 | }
113 | }
114 |
115 | class WaveBankHeader {
116 | constructor() {
117 | this.version = 0;
118 | this.segments = [];
119 | }
120 | }
121 |
122 | class WaveBankData {
123 | constructor() {
124 | this.flags = 0;
125 | this.entryCount = 0;
126 | this.bankName = '';
127 | this.entryMetaDataElementSize = 0;
128 | this.entryNameElementSize = 0;
129 | this.alignment = 0;
130 | this.compactFormat = 0;
131 | this.buildTime = 0;
132 | }
133 | }
134 |
135 | module.exports = Xact;
136 |
--------------------------------------------------------------------------------
/app/Xnb/Readers/Texture2DReader.js:
--------------------------------------------------------------------------------
1 | const BaseReader = require('./BaseReader');
2 | const BufferReader = require('../../BufferReader');
3 | const Int32Reader = require('./Int32Reader');
4 | const UInt32Reader = require('./UInt32Reader');
5 | const dxt = require('dxt-js');
6 | const Log = require('../../Log');
7 | const XnbError = require('../../XnbError');
8 |
9 | /**
10 | * Texture2D Reader
11 | * @class
12 | * @extends BaseReader
13 | */
14 | class Texture2DReader extends BaseReader {
15 | /**
16 | * Reads Texture2D from buffer.
17 | * @param {BufferReader} buffer
18 | * @returns {object}
19 | */
20 | read(buffer) {
21 | const int32Reader = new Int32Reader();
22 | const uint32Reader = new UInt32Reader();
23 |
24 | let format = int32Reader.read(buffer);
25 | let width = uint32Reader.read(buffer);
26 | let height = uint32Reader.read(buffer);
27 | let mipCount = uint32Reader.read(buffer);
28 |
29 | if (mipCount > 1)
30 | Log.warn(`Found mipcount of ${mipCount}, only the first will be used.`);
31 |
32 | let dataSize = uint32Reader.read(buffer);
33 | let data = buffer.read(dataSize);
34 |
35 | if (format == 4)
36 | data = dxt.decompress(data, width, height, dxt.flags.DXT1);
37 | else if (format == 5)
38 | data = dxt.decompress(data, width, height, dxt.flags.DXT3);
39 | else if (format == 6)
40 | data = dxt.decompress(data, width, height, dxt.flags.DXT5);
41 | else if (format == 2) {
42 | // require('fs').writeFileSync('texture.bin', data);
43 | throw new XnbError('Texture2D format type ECT1 not implemented!');
44 | }
45 | else if (format != 0)
46 | throw new XnbError(`Non-implemented Texture2D format type (${format}) found.`);
47 |
48 | // add the alpha channel into the image
49 | for(let i = 0; i < data.length; i += 4) {
50 | let inverseAlpha = 255 / data[i + 3];
51 | data[i ] = Math.min(Math.ceil(data[i ] * inverseAlpha), 255);
52 | data[i + 1] = Math.min(Math.ceil(data[i + 1] * inverseAlpha), 255);
53 | data[i + 2] = Math.min(Math.ceil(data[i + 2] * inverseAlpha), 255);
54 | }
55 |
56 | return {
57 | format,
58 | export: {
59 | type: this.type,
60 | data,
61 | width,
62 | height
63 | }
64 | };
65 | }
66 |
67 | /**
68 | * Writes Texture2D into the buffer
69 | * @param {BufferWriter} buffer
70 | * @param {Mixed} data The data
71 | * @param {ReaderResolver} resolver
72 | */
73 | write(buffer, content, resolver) {
74 | const int32Reader = new Int32Reader();
75 | const uint32Reader = new UInt32Reader();
76 |
77 | this.writeIndex(buffer, resolver);
78 |
79 | const width = content.export.width;
80 | const height = content.export.height;
81 |
82 | Log.debug(`Width: ${width}, Height: ${height}`);
83 | Log.debug(`Format: ${content.format}`);
84 |
85 | int32Reader.write(buffer, content.format, null);
86 | uint32Reader.write(buffer, content.export.width, null);
87 | uint32Reader.write(buffer, content.export.height, null);
88 | uint32Reader.write(buffer, 1, null);
89 |
90 | let data = content.export.data;
91 |
92 | for (let i = 0; i < data.length; i += 4) {
93 | const alpha = data[i + 3] / 255;
94 | data[i ] = Math.floor(data[i ] * alpha);
95 | data[i + 1] = Math.floor(data[i + 1] * alpha);
96 | data[i + 2] = Math.floor(data[i + 2] * alpha);
97 | }
98 |
99 | if (content.format == 4)
100 | data = dxt.compress(data, width, height, dxt.flags.DXT1);
101 | else if (content.format == 5)
102 | data = dxt.compress(data, width, height, dxt.flags.DXT3);
103 | else if (content.format == 6)
104 | data = dxt.compress(data, width, height, dxt.flags.DXT5);
105 |
106 | uint32Reader.write(buffer, data.length, null);
107 | buffer.concat(data);
108 | }
109 |
110 | isValueType() {
111 | return false;
112 | }
113 | }
114 |
115 | module.exports = Texture2DReader;
116 |
--------------------------------------------------------------------------------
/app/BufferWriter.js:
--------------------------------------------------------------------------------
1 | class BufferWriter {
2 |
3 | constructor(size = 500) {
4 | // the buffer to write to
5 | this._buffer = Buffer.alloc(size);
6 | // the current byte position
7 | this.bytePosition = 0;
8 | }
9 |
10 | /**
11 | * Returns the buffer.
12 | * @public
13 | * @property buffer
14 | * @returns {Buffer} Returns the internal buffer.
15 | */
16 | get buffer() {
17 | return this._buffer;
18 | }
19 |
20 | // trim the buffer to the byte position
21 | trim() {
22 | const tBuffer = Buffer.alloc(Number.parseInt(this.bytePosition));
23 | this._buffer.copy(tBuffer, 0, 0, this.bytePosition);
24 | this._buffer = tBuffer;
25 | }
26 |
27 | /**
28 | * Allocates number of bytes into the buffer and assigns more space if needed
29 | * @param {Number} bytes Number of bytes to allocate into the buffer
30 | */
31 | alloc(bytes) {
32 | if (this._buffer.length < this.bytePosition + bytes) {
33 | let tBuffer = Buffer.alloc(Math.max(this._buffer.length * 2, this._buffer.length + bytes));
34 | this._buffer.copy(tBuffer, 0);
35 | this._buffer = tBuffer;
36 | }
37 | return this;
38 | }
39 |
40 | concat(buffer) {
41 | this.alloc(buffer.length);
42 | this._buffer.set(buffer, this.bytePosition);
43 | this.bytePosition += buffer.length;
44 | }
45 |
46 | /**
47 | * Writes bytes to the buffer
48 | * @param {Mixed} string
49 | */
50 | write(string, length = Buffer.byteLength(string)) {
51 | this.alloc(length).buffer.write(string, this.bytePosition);
52 | this.bytePosition += length;
53 | }
54 |
55 | /**
56 | * Write a byte to the buffer
57 | * @param {Mixed} byte
58 | */
59 | writeByte(byte) {
60 | this.alloc(1).buffer.writeUInt8(byte, this.bytePosition);
61 | this.bytePosition++;
62 | }
63 |
64 | /**
65 | * Write an int8 to the buffer
66 | * @param {Number} number
67 | */
68 | writeInt(number) {
69 | this.alloc(1).buffer.writeInt8(byte, this.bytePosition);
70 | this.bytePosition++;
71 | }
72 |
73 | /**
74 | * Write a uint8 to the buffer
75 | * @param {Number} number
76 | */
77 | writeUInt(number) {
78 | this.alloc(1).buffer.writeUInt8(byte, this.bytePosition);
79 | this.bytePosition++;
80 | }
81 |
82 | /**
83 | * Write a int16 to the buffer
84 | * @param {Number} number
85 | */
86 | writeInt16(number) {
87 | this.alloc(2).buffer.writeInt16(byte, this.bytePosition);
88 | this.bytePosition += 2;
89 | }
90 |
91 | /**
92 | * Write a uint16 to the buffer
93 | * @param {Number} number
94 | */
95 | writeUInt16(number) {
96 | this.alloc(2).buffer.writeUInt16(byte, this.bytePosition);
97 | this.bytePosition += 2;
98 | }
99 |
100 | /**
101 | * Write a int32 to the buffer
102 | * @param {Number} number
103 | */
104 | writeInt32(number) {
105 | this.alloc(4).buffer.writeInt32LE(number, this.bytePosition);
106 | this.bytePosition += 4;
107 | }
108 |
109 | /**
110 | * Write a uint32 to the buffer
111 | * @param {Number} number
112 | */
113 | writeUInt32(number) {
114 | this.alloc(4).buffer.writeUInt32LE(number, this.bytePosition);
115 | this.bytePosition += 4;
116 | }
117 |
118 | /**
119 | * Write a float to the buffer
120 | * @param {Number} number
121 | */
122 | writeSingle(number) {
123 | this.alloc(4).buffer.writeFloatLE(number, this.bytePosition);
124 | this.bytePosition += 4;
125 | }
126 |
127 | /**
128 | * Write a double to the buffer
129 | * @param {Number} number
130 | */
131 | writeDouble(number) {
132 | this.alloc(4).buffer.writeDoubleLE(number, this.bytePosition);
133 | this.bytePosition += 4;
134 | }
135 |
136 | /**
137 | * Write a 7-bit number to the buffer
138 | * @param {Number} number
139 | */
140 | write7BitNumber(number) {
141 | this.alloc(2);
142 | do {
143 | let byte = number & 0x7F;
144 | number = number >> 7;
145 | if (number) byte |= 0x80;
146 | this.buffer.writeUInt8(byte, this.bytePosition);
147 | this.bytePosition++;
148 | }
149 | while (number);
150 | }
151 |
152 | }
153 |
154 | // export the BufferWriter class
155 | module.exports = BufferWriter;
156 |
--------------------------------------------------------------------------------
/app/Xnb/TypeReader.js:
--------------------------------------------------------------------------------
1 | const Log = require('../Log');
2 | const BufferReader = require('../BufferReader');
3 | const ReaderResolver = require('./ReaderResolver');
4 | const XnbError = require('../XnbError');
5 | const Readers = require('./Readers');
6 |
7 | /**
8 | * Used to simplify type from XNB file.
9 | * @function simplifyType
10 | * @param {String} type The long verbose type read from XNB file.
11 | * @returns {String} returns shorthand simplified type for use within this tool.
12 | */
13 | const simplifyType = type => {
14 | // gets the first part of the type
15 | let simple = type.split(/`|,/)[0];
16 |
17 | Log.debug(`Type: ${simple}`);
18 |
19 | // check if its an array or not
20 | let isArray = simple.endsWith('[]');
21 | // if its an array then get the array type
22 | if (isArray)
23 | return `Array<${simplifyType(simple.slice(0, -2))}>`;
24 |
25 | // switch over the possible types regisered with this tool
26 | switch (simple) {
27 |
28 | // Boolean
29 | case 'Microsoft.Xna.Framework.Content.BooleanReader':
30 | case 'System.Boolean':
31 | return 'Boolean';
32 |
33 | // Char
34 | case 'Microsoft.Xna.Framework.Content.CharReader':
35 | case 'System.Char':
36 | return 'Char';
37 |
38 | // Int32
39 | case 'Microsoft.Xna.Framework.Content.Int32Reader':
40 | case 'System.Int32':
41 | return 'Int32';
42 |
43 | // String
44 | case 'Microsoft.Xna.Framework.Content.StringReader':
45 | case 'System.String':
46 | return 'String';
47 |
48 | // Dictionary
49 | case 'Microsoft.Xna.Framework.Content.DictionaryReader':
50 | let subtypes = parseSubtypes(type).map(simplifyType);
51 | return `Dictionary<${subtypes[0]},${subtypes[1]}>`;
52 |
53 | // Array
54 | case 'Microsoft.Xna.Framework.Content.ArrayReader':
55 | let arrayType = parseSubtypes(type).map(simplifyType);
56 | return `Array<${arrayType}>`;
57 |
58 | // List
59 | case 'Microsoft.Xna.Framework.Content.ListReader':
60 | case 'System.Collections.Generic.List':
61 | let listType = parseSubtypes(type).map(simplifyType);
62 | return `List<${listType}>`;
63 |
64 | // Texture2D
65 | case 'Microsoft.Xna.Framework.Content.Texture2DReader':
66 | return 'Texture2D';
67 |
68 | // Vector2
69 | case 'Microsoft.Xna.Framework.Content.Vector2Reader':
70 | case 'Microsoft.Xna.Framework.Vector2':
71 | return 'Vector2';
72 |
73 | // Vector3
74 | case 'Microsoft.Xna.Framework.Content.Vector3Reader':
75 | case 'Microsoft.Xna.Framework.Vector3':
76 | return 'Vector3';
77 |
78 | // Vector3
79 | case 'Microsoft.Xna.Framework.Content.Vector4Reader':
80 | case 'Microsoft.Xna.Framework.Vector4':
81 | return 'Vector4';
82 |
83 | // SpriteFont
84 | case 'Microsoft.Xna.Framework.Content.SpriteFontReader':
85 | return 'SpriteFont';
86 |
87 | // Rectangle
88 | case 'Microsoft.Xna.Framework.Content.RectangleReader':
89 | case 'Microsoft.Xna.Framework.Rectangle':
90 | return 'Rectangle';
91 |
92 | // Effect
93 | case 'Microsoft.Xna.Framework.Content.EffectReader':
94 | case 'Microsoft.Xna.Framework.Graphics.Effect':
95 | return 'Effect';
96 |
97 | // xTile TBin
98 | case 'xTile.Pipeline.TideReader':
99 | return 'TBin';
100 |
101 | // BmFont
102 | case 'BmFont.XmlSourceReader':
103 | return 'BmFont';
104 |
105 | case 'Microsoft.Xna.Framework.Content.SoundEffectReader':
106 | case 'Microsoft.Xna.Framework.SoundEffect':
107 | return 'SoundEffect';
108 |
109 | // unimplemented type catch
110 | default:
111 | throw new XnbError(`Non-implemented type found, cannot resolve type "${simple}", "${type}".`);
112 | }
113 | }
114 |
115 | exports.simplifyType = simplifyType;
116 |
117 | /**
118 | * Parses subtypes from a type like Dictionary or List
119 | * @function parseSubtypes
120 | * @param {String} type The type to parse with subtypes in.
121 | * @returns {String[]} returns an array of subtypes
122 | */
123 | const parseSubtypes = type => {
124 | // split the string by the ` after the type
125 | let subtype = type.split('`')[1];
126 | // get the number of types following the ` in type string
127 | let count = subtype.slice(0, 1);
128 |
129 | // get the contents of the wrapped array
130 | subtype = subtype.slice(2, -1);
131 |
132 | // regex pattern to match the subtypes
133 | let pattern = /\[(([a-zA-Z0-9\.\,\=\`]+)(\[\])?(\, |\])){1,}/g;
134 | // get matches
135 | let matches = subtype.match(pattern).map(e => {
136 | return e.slice(1, -1);
137 | });
138 |
139 | // return the matches
140 | return matches;
141 | }
142 |
143 | exports.parseSubtypes = parseSubtypes;
144 |
145 | /**
146 | * Get type info from simple type
147 | * @param {String} type Simple type to get info from.
148 | * @returns {Object} returns an object containing information about the type.
149 | */
150 | const getTypeInfo = type => {
151 | // get type before angle brackets for complex types
152 | let mainType = type.match(/[^<]+/)[0];
153 | // get the subtypes within brackets
154 | let subtypes = type.match(/<(.+)>/);
155 |
156 | // if we do have subtypes then split and trim them
157 | subtypes = subtypes ? subtypes[1].split(',').map(type => type.trim()) : [];
158 |
159 | // return info object
160 | return { type: mainType, subtypes };
161 | }
162 |
163 | exports.getTypeInfo = getTypeInfo;
164 |
165 | /**
166 | * Gets an XnbReader instance based on type.
167 | * @function getReader
168 | * @param {String} type The simplified type to get reader based off of.
169 | * @returns {BaseReader} returns an instance of BaseReader for given type.
170 | */
171 | const getReader = type => {
172 | // get type info for complex types
173 | let info = getTypeInfo(type);
174 | // loop over subtypes and resolve readers for them
175 | info.subtypes = info.subtypes.map(getReader);
176 |
177 | // if we have a reader then use one
178 | if (Readers.hasOwnProperty(`${info.type}Reader`))
179 | return new (Readers[`${info.type}Reader`])(info.subtypes[0], info.subtypes[1]);
180 |
181 | // throw an error as type is not supported
182 | throw new XnbError(`Invalid reader type "${type}" passed, unable to resolve!`);
183 | }
184 |
185 | exports.getReader = getReader;
186 |
--------------------------------------------------------------------------------
/LICENSE.LESSER:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/app/Xact/SoundBank.js:
--------------------------------------------------------------------------------
1 | const Log = require('../Log');
2 | const BufferReader = require('../BufferReader');
3 | const XnbError = require('../XnbError');
4 | const Enum = require('../Enum');
5 | const Struct = require('../Struct');
6 | const XactSound = require('./XactSound');
7 | const chalk = require('chalk');
8 |
9 | // SoundBank Constants
10 | const SDBK_FORMAT_VERSION = 0x2B;
11 |
12 | /**
13 | * SoundBank is used for XACT Sound Bank files.
14 | * @public
15 | * @class
16 | */
17 | class SoundBank {
18 | /**
19 | * Load the specified file.
20 | * @public
21 | * @param {String} filename
22 | */
23 | static load(filename) {
24 | // create a buffer and load the file
25 | try {
26 | // create a new buffer to work with
27 | const buffer = new BufferReader(filename);
28 | // process the file
29 | this._proccess(buffer);
30 | }
31 | catch (ex) {
32 | Log.error(`Exception caught while loading ${filename} into AudioEngine`);
33 | Log.error(ex.stack);
34 | }
35 | }
36 |
37 | /**
38 | * Proccess the SoundBank
39 | * @private
40 | * @param {BufferReader} buffer
41 | */
42 | static _proccess(buffer) {
43 | const magic = buffer.read(4);
44 | if (magic != 'SDBK')
45 | throw new XnbError(`Invalid magic found, ${magic}`);
46 |
47 | const toolVersion = buffer.readUInt16();
48 | const formatVersion = buffer.readUInt16();
49 |
50 | if (formatVersion != SDBK_FORMAT_VERSION)
51 | Log.warn(`SoundBank format ${formatVersion} not supported.`);
52 |
53 | Log.debug(`Format Version: ${Log.h(formatVersion)}`);
54 | Log.debug(`Tool Version: ${Log.h(toolVersion)}`);
55 |
56 | // fcs16 checksum for following data
57 | // NOTE: giving zero fucks about CRC
58 | const crc = buffer.readUInt16();
59 | Log.debug(`CRC: ${Log.h(crc)}`);
60 |
61 | const lastModifiedLow = buffer.readUInt32();
62 | const lastModifiedHigh = buffer.readUInt32();
63 | const platform = buffer.readByte();
64 |
65 | Log.debug(`LML: ${lastModifiedLow}, LMH: ${lastModifiedHigh}`);
66 | Log.debug(`Platform: ${Log.h(platform)}`);
67 |
68 | const numSimpleCues = buffer.readUInt16();
69 | const numComplexCues = buffer.readUInt16();
70 | buffer.seek(2);
71 | const numTotalCues = buffer.readUInt16();
72 | const numWaveBanks = buffer.readByte();
73 | const numSounds = buffer.readUInt16();
74 | const cueNameTableLen = buffer.readUInt16();
75 | buffer.seek(2);
76 |
77 | Log.debug(`Simple Cues: ${numSimpleCues}`);
78 | Log.debug(`Complex Cues: ${numComplexCues}`);
79 | Log.debug(`Total Cues: ${numTotalCues}`);
80 | Log.debug(`Wave Banks: ${numWaveBanks}`);
81 | Log.debug(`Sounds: ${numSounds}`);
82 | Log.debug(`Cue Name Table Length: ${cueNameTableLen}`);
83 |
84 | const simpleCuesOffset = buffer.readUInt32();
85 | const complexCuesOffset = buffer.readUInt32();
86 | const cueNamesOffset = buffer.readUInt32();
87 | buffer.seek(4);
88 | const variationTablesOffset = buffer.readUInt32();
89 | buffer.seek(4);
90 | const waveBankNameTableOffset = buffer.readUInt32();
91 | const cueNameHashTableOffset = buffer.readUInt32();
92 | const cueNameHashValsOffset = buffer.readUInt32();
93 | const soundsOffset = buffer.readUInt32();
94 |
95 | Log.debug(`Simple Cues Offset: ${simpleCuesOffset}`);
96 | Log.debug(`Complex Cues Offset: ${complexCuesOffset}`);
97 | Log.debug(`Cue Names Offset: ${cueNamesOffset}`);
98 | Log.debug(`Variation Table Offset: ${variationTablesOffset}`);
99 | Log.debug(`Wave Bank Name Table Offset: ${waveBankNameTableOffset}`);
100 | Log.debug(`Cue Name Hash Table Offset: ${cueNameHashTableOffset}`);
101 | Log.debug(`Cue Name Hash Values Offset: ${cueNameHashValsOffset}`);
102 | Log.debug(`Sounds Offset: ${soundsOffset}`);
103 |
104 | const name = buffer.read(64);
105 | Log.debug(`Name: ${name}`);
106 |
107 | // parse wave bank name table
108 | buffer.seek(waveBankNameTableOffset, 0);
109 | const waveBanks = new Array(numWaveBanks);
110 | const waveBankNames = new Array(numWaveBanks);
111 | for (let i = 0; i < numWaveBanks; i++)
112 | waveBankNames[i] = buffer.read(64);
113 |
114 | Log.debug(`Wave Banks: ${waveBankNames}`);
115 |
116 | // parse cue name table
117 | buffer.seek(cueNamesOffset, 0);
118 | const cueNames = buffer.read(cueNameTableLen).toString().split('\0').slice(0, -1);
119 | buffer.seek(simpleCuesOffset, 0);
120 | for (let i = 0; i < numSimpleCues; i++) {
121 | const flags = buffer.read(1);
122 | const soundOffset = buffer.readUInt32();
123 | }
124 | Log.debug(`Cues: ${cueNames}`);
125 |
126 | let totalCueCount = 0;
127 |
128 | buffer.seek(complexCuesOffset, 0);
129 | for (let i = 0; i < numComplexCues; i++) {
130 | const flags = buffer.readByte();
131 |
132 | let cue;
133 |
134 | Log.debug();
135 |
136 | if (((flags >> 2) & 1) != 0) {
137 | // not sure :/
138 | const soundOffset = buffer.readUInt32();
139 | buffer.seek(4);
140 |
141 | Log.debug(`Found ${chalk.bold.green(cueNames[numSimpleCues+i])}`);
142 | const sound = new XactSound(buffer, soundOffset);
143 | totalCueCount++;
144 | }
145 | else {
146 | const variationTableOffset = buffer.readUInt32();
147 | const transitionTableOffset = buffer.readUInt32();
148 |
149 | const savepos = buffer.bytePosition;
150 | // parse variation table
151 | buffer.seek(variationTableOffset, 0);
152 |
153 | const numEntries = buffer.readUInt16();
154 | const variationFlags = buffer.readUInt16();
155 | buffer.seek(4);
156 |
157 | const cueSounds = new Array(numEntries);
158 | Log.debug(`Found ${chalk.bold.yellow(numEntries)} cues for ${chalk.bold.green(cueNames[numSimpleCues+i])}`);
159 |
160 | const tableType = (variationFlags >> 3) & 0x7;
161 | for (let j = 0; j < numEntries; j++) {
162 | switch (tableType) {
163 | case 0: { // Wave
164 | const trackIndex = buffer.readUInt16();
165 | const waveBankIndex = buffer.readByte();
166 | const weightMin = buffer.readByte();
167 | const weightMax = buffer.readByte();
168 | Log.debug(chalk.inverse(`WaveBank Index: ${waveBankIndex}`));
169 | break;
170 | }
171 | case 1: {
172 | const soundOffset = buffer.readUInt32();
173 | const weightMin = buffer.readByte();
174 | const weightMax = buffer.readByte();
175 | cueSounds[j] = new XactSound(buffer, soundOffset);
176 | totalCueCount++;
177 | break;
178 | }
179 | case 4: { // CompactWave
180 | const trackIndex = buffer.readUInt16();
181 | const waveBankIndex = buffer.readByte();
182 | Log.debug(chalk.inverse(`WaveBank Index: ${waveBankIndex}`));
183 | break;
184 | }
185 | default:
186 | throw new Error('Table type not implemented');
187 | }
188 | }
189 | buffer.seek(savepos, 0);
190 | }
191 |
192 | // instance limit
193 | buffer.seek(6);
194 | }
195 | Log.debug(`Found ${chalk.bold.red(totalCueCount)} cues!`);
196 | }
197 | }
198 |
199 | module.exports = SoundBank;
200 |
--------------------------------------------------------------------------------
/app/Xact/AudioEngine.js:
--------------------------------------------------------------------------------
1 | const Log = require('../Log');
2 | const BufferReader = require('../BufferReader');
3 | const XnbError = require('../XnbError');
4 | const Enum = require('../Enum');
5 | const Struct = require('../Struct');
6 |
7 | // Audio Engine Constants
8 | const XGSF_FORMAT = 0x2A;
9 |
10 | // Structs and Enums used for AudioEngine
11 | const Variable = Struct({
12 | name: '',
13 | value: 0.0,
14 |
15 | isGlobal: false,
16 | isReadOnly: false,
17 | isPublic: false,
18 | isReserved: false,
19 |
20 | initValue: 0.0,
21 | maxValue: 0.0,
22 | minValue: 0.0
23 | });
24 |
25 | const RpcPointType = Enum([
26 | 'Linear',
27 | 'Fast',
28 | 'Slow',
29 | 'SinCos'
30 | ]);
31 |
32 | const RpcPoint = Struct({
33 | x: 0.0,
34 | y: 0.0,
35 | type: undefined
36 | });
37 |
38 | const RpcParameter = Enum([
39 | 'Volume',
40 | 'Pitch',
41 | 'ReverbSend',
42 | 'FilterFrequency',
43 | 'FilterQFactor'
44 | ]);
45 |
46 | const RpcCurve = Struct({
47 | variable: 0,
48 | parameter: undefined,
49 | points: []
50 | });
51 |
52 | /**
53 | * AudioEngine class is used to load XACT XGS files
54 | * @public
55 | * @class
56 | */
57 | class AudioEngine {
58 | /**
59 | * Load the specified file.
60 | * @public
61 | * @param {String} filename
62 | */
63 | load(filename) {
64 | // create a buffer and load the file
65 | try {
66 | // create a new buffer to work with
67 | const buffer = new BufferReader(filename);
68 | // process the file
69 | this.proccess(buffer);
70 | }
71 | catch (ex) {
72 | Log.error(`Exception caught while loading ${filename} into AudioEngine`);
73 | Log.error(ex.stack);
74 | }
75 | }
76 |
77 | /**
78 | * Processes the file.
79 | * @private
80 | * @param {BufferReader} buffer
81 | */
82 | proccess(buffer) {
83 | // read in the magic
84 | const magic = buffer.read(4);
85 | // ensure the magic matches
86 | if (magic != 'XGSF')
87 | throw new XnbError(`Invalid magic found, ${magic}`);
88 |
89 | // read in tool and format versions
90 | const toolVersion = buffer.readUInt16();
91 | const formatVersion = buffer.readUInt16();
92 |
93 | // see if we have a known format
94 | if (formatVersion != XGSF_FORMAT)
95 | Log.warn(`XGS format not supported!`);
96 |
97 | // log the versions
98 | Log.debug(`Tool Version: ${toolVersion}`);
99 | Log.debug(`Format Version: ${formatVersion}`);
100 |
101 | // get the useless CRC that we don't care about
102 | const crc = buffer.readUInt16();
103 |
104 | // get the last modified low and high values
105 | const lastModifiedLow = buffer.readUInt32();
106 | const lastModifiedHigh = buffer.readUInt32();
107 |
108 | // skip unknown byte (possibly platform)
109 | buffer.seek(1);
110 |
111 | // read the number of categories and variables
112 | const numCats = buffer.readUInt16();
113 | const numVars = buffer.readUInt16();
114 |
115 | Log.debug(`Categories: ${numCats}`);
116 | Log.debug(`Variables: ${numVars}`);
117 |
118 | // skip past two unknown 16-bit integers
119 | buffer.seek(4);
120 |
121 | // read number of RPC, DSP presets and params
122 | const numRpc = buffer.readUInt16();
123 | const numDspPresets = buffer.readUInt16();
124 | const numDspParams = buffer.readUInt16();
125 |
126 | Log.debug(`RPC: ${numRpc}`);
127 | Log.debug(`DSP Presets: ${numDspPresets}`);
128 | Log.debug(`DSP Params: ${numDspParams}`);
129 |
130 | // get the offset for the categories and variables
131 | const catsOffset = buffer.readUInt32();
132 | const varsOffset = buffer.readUInt32();
133 |
134 | Log.debug(`Category Offset: ${catsOffset}`);
135 | Log.debug(`Variables Offset: ${varsOffset}`);
136 |
137 | // unknown 32-bit uint
138 | buffer.seek(4);
139 | // get category name index offset
140 | const catNameIndexOffset = buffer.readUInt32();
141 | // unknown 32-bit uint
142 | buffer.seek(4);
143 | // get variable name index offset
144 | const varNameIndexOffset = buffer.readUInt32();
145 |
146 | Log.debug(`Category Name Index Offset: ${catNameIndexOffset}`);
147 | Log.debug(`Variable Name Index Offset: ${varNameIndexOffset}`);
148 |
149 | // read in the category and variable names offsets
150 | const catNamesOffset = buffer.readUInt32();
151 | const varNamesOffset = buffer.readUInt32();
152 |
153 | // read in RPC, DSP preset and params offsets
154 | const rpcOffset = buffer.readUInt32();
155 | const dspPresetOffset = buffer.readUInt32();
156 | const dspParamsOffset = buffer.readUInt32();
157 |
158 | Log.debug(`Category Names Offset: ${catNamesOffset}`);
159 | Log.debug(`Variables Names Offset: ${varNamesOffset}`);
160 | Log.debug(`RPC Offset: ${rpcOffset}`);
161 | Log.debug(`DSP Preset Offset: ${dspPresetOffset}`);
162 | Log.debug(`DSP Params Offset: ${dspParamsOffset}`);
163 |
164 | // seek to the category name offset to read in the categories
165 | buffer.seek(catNamesOffset, 0);
166 | const categoryNames = this._readNullTerminatedStrings(buffer, numCats);
167 | Log.debug(`Categories: ${categoryNames}`);
168 |
169 | // get the actual category data
170 | //const categories = new Array(numCats);
171 |
172 | // seek to the variable names offset
173 | buffer.seek(varNamesOffset, 0);
174 | // read in the variable names
175 | const varNames = this._readNullTerminatedStrings(buffer, numVars);
176 | Log.debug(`Variables: ${varNames}`);
177 |
178 | // read in the variables themselves
179 | const variables = new Array(numVars);
180 | // seek to the variable offset
181 | buffer.seek(varsOffset, 0);
182 | // loop over the variables
183 | for (let i = 0; i < numVars; i++) {
184 | // create the variable for this index
185 | variables[i] = new Variable({ name: varNames[i] });
186 |
187 | const flags = buffer.readByte();
188 | variables[i].isPublic = (flags & 0x1) != 0;
189 | variables[i].isReadOnly = (flags & 0x2) != 0;
190 | variables[i].isGlobal = (flags & 0x4) != 0;
191 | variables[i].isReserved = (flags & 0x8) != 0;
192 |
193 | variables[i].initValue = buffer.readSingle();
194 | variables[i].minValue = buffer.readSingle();
195 | variables[i].maxValue = buffer.readSingle();
196 |
197 | variables[i].value = variables[i].initValue;
198 | }
199 |
200 | Log.debug(JSON.stringify(variables, null, 4));
201 |
202 | // RPC curves
203 | const rpcCurves = new Array(numRpc);
204 | buffer.seek(rpcOffset, 0);
205 | for (let i = 0; i < numRpc; i++) {
206 | rpcCurves[i] = new RpcCurve({ variable: buffer.readUInt16() });
207 | const pointCount = buffer.readByte();
208 | rpcCurves[i].parameter = buffer.readUInt16();
209 | rpcCurves[i].points = new Array(pointCount);
210 | for (let j = 0; j < pointCount; j++) {
211 | rpcCurves[i].points[j] = new RpcPoint({
212 | x: buffer.readSingle(),
213 | y: buffer.readSingle(),
214 | type: buffer.readByte(),
215 | });
216 | }
217 | }
218 |
219 | Log.debug(JSON.stringify(rpcCurves, null, 4));
220 | }
221 |
222 | /**
223 | * Reads null terminated strings from buffer.
224 | * @private
225 | * @param {BufferReader} buffer
226 | * @param {Number} count
227 | * @returns {String[]}
228 | */
229 | _readNullTerminatedStrings(buffer, count) {
230 | Log.debug(`Reading ${count} strings`);
231 | const ret = new Array(count);
232 | for (let i = 0; i < count; i++)
233 | ret[i] = buffer.readString();
234 | return ret;
235 | }
236 | }
237 |
238 | module.exports = AudioEngine;
239 |
--------------------------------------------------------------------------------
/xnbcli.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { program } = require('commander');
4 | const Log = require('./app/Log');
5 | const Xnb = require('./app/Xnb');
6 | const { exportFile, resolveImports } = require('./app/Porter');
7 | const chalk = require('chalk');
8 | const mkdirp = require('mkdirp');
9 | const walk = require('walk');
10 | const got = require('got');
11 | const compareVersions = require('compare-versions');
12 |
13 | // used for displaying the tally of success and fail
14 | let success = 0;
15 | let fail = 0;
16 |
17 | // define the version number
18 | const VERSION = '1.0.7';
19 |
20 | // check for updates
21 | async function checkUpdate() {
22 |
23 | try {
24 | // fetch the package.json to see if there's a new version available
25 | const response = await got('https://raw.githubusercontent.com/LeonBlade/xnbcli/master/package.json', { json: true });
26 | const remoteVersion = response.body.version;
27 |
28 | // compare remote version with the current version
29 | if (compareVersions(remoteVersion, VERSION) > 0) {
30 | // NOTE: this bugs the user every time they run the tool, not exactly a bad idea but maybe should think
31 | // of a different approach to not hit github every time? idk maybe it doesn't matter though
32 | Log.info(`${chalk.bold.green(`xnbcli v${remoteVersion} is available!`)} Visit ${chalk.blue('https://github.com/LeonBlade/xnbcli/releases')} to get the latest release.`);
33 | }
34 | }
35 | catch (error) {
36 | Log.error('Failed to search for a new update. Application should still function normally.');
37 | Log.error(error);
38 | }
39 | }
40 |
41 | (() => {
42 | // call the init function to get the party started
43 | init();
44 |
45 | })();
46 |
47 | // initialize function called after we fetch the newest version
48 | function init() {
49 | // create the program and set version number
50 | program.version(VERSION);
51 |
52 | // turn on debug printing
53 | program.option('--debug', 'Enables debug verbose printing.', () => Log.setMode(Log.DEBUG, true));
54 |
55 | // only display errors
56 | program.option('--errors', 'Only prints error messages.', () => Log.setMode(Log.INFO | Log.WARN | Log.DEBUG, false));
57 |
58 | // display nothing
59 | program.option('--silent', 'Prints nothing at all.', () => Log.setMode(Log.INFO | Log.WARN | Log.DEBUG | Log.ERROR, false));
60 |
61 | // XNB unpack command
62 | program
63 | .command('unpack [output]')
64 | .description('Used to unpack XNB files.')
65 | .action((input, output) => {
66 | // process the unpack
67 | processFiles(processUnpack, input, output, details);
68 | });
69 |
70 | // XNB pack Command
71 | program
72 | .command('pack [output]')
73 | .description('Used to pack XNB files.')
74 | .action((input, output) => {
75 | // process the pack
76 | processFiles(processPack, input, output, details);
77 | });
78 |
79 | // default action
80 | program.action(() => program.help());
81 |
82 | // parse the input and run the commander program
83 | program.parse(process.argv);
84 |
85 | // show help if we didn't specify any valid input
86 | if (!process.argv.slice(2).length)
87 | program.help();
88 | }
89 |
90 | /**
91 | * Display the results of the processing
92 | */
93 | function details() {
94 | // give a final analysis of the files
95 | console.log(`${chalk.bold.green('Success')} ${success}`);
96 | console.log(`${chalk.bold.red('Fail')} ${fail}`);
97 | }
98 |
99 | /**
100 | * Takes input and processes input for unpacking.
101 | * @param {String} input
102 | * @param {String} output
103 | */
104 | function processUnpack(input, output) {
105 | // catch any exceptions to keep a batch of files moving
106 | try {
107 | // ensure that the input file has the right extension
108 | if (path.extname(input).toLocaleLowerCase() != '.xnb')
109 | return;
110 |
111 | // create new instance of XNB
112 | const xnb = new Xnb();
113 |
114 | // load the XNB and get the object from it
115 | const result = xnb.load(input);
116 |
117 | // save the file
118 | if (!exportFile(output, result)) {
119 | Log.error(`File ${output} failed to save!`);
120 | return fail++;
121 | }
122 |
123 | // log that the file was saved
124 | Log.info(`Output file saved: ${output}`);
125 |
126 | // increase success count
127 | success++;
128 | }
129 | catch (ex) {
130 | // log out the error
131 | Log.error(`Filename: ${input}\n${ex.stack}\n`);
132 | // increase fail count
133 | fail++;
134 | }
135 | }
136 |
137 | /**
138 | * Process the pack of files to xnb
139 | * @param {String} input
140 | * @param {String} output
141 | * @param {Function} done
142 | */
143 | function processPack(input, output) {
144 | try {
145 | // ensure that the input file has the right extension
146 | if (path.extname(input).toLocaleLowerCase() != '.json')
147 | return;
148 |
149 | Log.info(`Reading file "${input}" ...`);
150 |
151 | // create instance of xnb
152 | const xnb = new Xnb();
153 |
154 | // resolve the imports
155 | const json = resolveImports(input);
156 | // convert the JSON to XNB
157 | const buffer = xnb.convert(json);
158 |
159 | // write the buffer to the output
160 | fs.writeFileSync(output, buffer);
161 |
162 | // log that the file was saved
163 | Log.info(`Output file saved: ${output}`);
164 |
165 | // increase success count
166 | success++;
167 | }
168 | catch (ex) {
169 | // log out the error
170 | Log.error(`Filename: ${input}\n${ex.stack}\n`);
171 | // increase fail count
172 | fail++;
173 | }
174 | }
175 |
176 | /**
177 | * Used to walk a path with input/output for processing
178 | * @param {Function} fn
179 | * @param {String} input
180 | * @param {String} output
181 | * @param {Function} cb
182 | */
183 | function processFiles(fn, input, output, cb) {
184 |
185 | // if this isn't a directory then just run the function
186 | if (!fs.statSync(input).isDirectory()) {
187 | // get the extension from the original path name
188 | const ext = path.extname(input);
189 | // get the new extension
190 | const newExt = (ext == '.xnb' ? '.json' : '.xnb');
191 |
192 | // output is undefined or is a directory
193 | if (output == undefined) {
194 | output = path.join(path.dirname(input), path.basename(input, ext) + newExt);
195 | }
196 | // output is a directory
197 | else if (fs.statSync(output).isDirectory())
198 | output = path.join(output, path.basename(input, ext) + newExt);
199 |
200 | // call the function
201 | return fn(input, output);
202 | }
203 |
204 | // output is undefined
205 | if (output == undefined)
206 | output = input;
207 |
208 | // get out grandpa's walker
209 | const walker = walk.walk(input);
210 |
211 | // when we encounter a file
212 | walker.on('file', (root, stats, next) => {
213 | // get the extension
214 | const ext = path.extname(stats.name).toLocaleLowerCase();
215 | // skip files that aren't JSON or XNB
216 | if (ext != '.json' && ext != '.xnb')
217 | return next();
218 |
219 | // swap the input base directory with the base output directory for our target directory
220 | const target = root.replace(input, output);
221 | // get the source path
222 | const inputFile = path.join(root, stats.name);
223 | // get the target ext
224 | const targetExt = ext == '.xnb' ? '.json' : '.xnb';
225 | // form the output file path
226 | const outputFile = path.join(target, path.basename(stats.name, ext) + targetExt);
227 |
228 | // ensure the path to the output file exists
229 | if (!fs.existsSync(path.dirname(inputFile)))
230 | mkdirp.sync(outputFile);
231 |
232 | // run the function
233 | fn(inputFile, outputFile);
234 | // next file
235 | next();
236 | });
237 |
238 | // any errors that happen
239 | walker.on('errors', (root, stats, next) => {
240 | next();
241 | });
242 |
243 | // done walking the dog
244 | walker.on('end', cb);
245 | }
246 |
--------------------------------------------------------------------------------
/app/Porter.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const mkdirp = require('mkdirp');
4 | const PNG = require('pngjs').PNG;
5 | const Log = require('./Log');
6 | const XnbError = require('./XnbError');
7 |
8 | /**
9 | * Used to save a parsed XNB file.
10 | * @param {object} xnbObject
11 | * @returns {Boolean}
12 | */
13 | const exportFile = (filename, xnbObject) => {
14 | // get the dirname for the file
15 | const dirname = path.dirname(filename);
16 | // get the basename for the file
17 | const basename = path.basename(filename, '.json');
18 |
19 | // create folder path if it doesn't exist
20 | if (!fs.existsSync(dirname))
21 | mkdirp.sync(dirname);
22 |
23 | // ensure we have content field
24 | if (!xnbObject.hasOwnProperty('content'))
25 | throw new XnbError('Invalid object!');
26 |
27 | // pull reference of content out of data
28 | const content = xnbObject.content;
29 | // search the content object for exports to process
30 | const found = search(content, 'export');
31 |
32 | // if we found data to export
33 | if (found) {
34 | // get the key path from found
35 | const keyPath = found.path;
36 | // get the exported buffer from found
37 | const exported = found.value;
38 |
39 | if (exported == undefined || exported.type == undefined || exported.data == undefined)
40 | throw new XnbError('Invalid file export!');
41 |
42 | // log that we are exporting additional data
43 | Log.info(`Exporting ${exported.type} ...`);
44 |
45 | // set buffer by default
46 | let buffer = exported.data;
47 | // set extension by default
48 | let extension = 'bin';
49 |
50 | // resolve found content based on key path if empty then its just content
51 | const foundContent = (keyPath.length ? content[keyPath] : content);
52 |
53 | // switch over possible export types
54 | // TODO: make this a litle cleaner possibly with its own function
55 | switch (exported.type) {
56 | // Texture2D to PNG
57 | case 'Texture2D':
58 | buffer = toPNG(
59 | exported.width,
60 | exported.height,
61 | exported.data
62 | );
63 |
64 | extension = 'png';
65 | break;
66 |
67 | // Compiled Effects
68 | case 'Effect':
69 | extension = 'cso';
70 | break;
71 |
72 | // TODO: TBin to tbin or tmx
73 | case 'TBin':
74 | extension = 'tbin';
75 | break;
76 |
77 | // BmFont Xml
78 | case 'BmFont':
79 | extension = 'xml';
80 | break;
81 |
82 | case 'SoundEffect':
83 | extension = 'wav';
84 | }
85 |
86 | // output file name
87 | const outputFilename = path.resolve(dirname, `${basename}.${extension}`);
88 |
89 | // save the file
90 | fs.writeFileSync(outputFilename, buffer);
91 |
92 | // set the exported value to the path
93 | foundContent['export'] = path.basename(outputFilename);
94 | }
95 |
96 | // save the XNB object as JSON
97 | fs.writeFileSync(filename, JSON.stringify(xnbObject, null, 4));
98 |
99 | // successfully exported file(s)
100 | return true;
101 | }
102 |
103 | exports.exportFile = exportFile;
104 |
105 | /**
106 | * Resolves all exported content back into the object
107 | * @param {String} filename
108 | * @returns {Object}
109 | */
110 | const resolveImports = filename => {
111 | // get the directory name
112 | const dirname = path.dirname(filename);
113 | // get the basename for the file
114 | const basename = path.basename(filename);
115 |
116 | // read in the file contents
117 | const buffer = fs.readFileSync(filename);
118 | // get the JSON for the contents
119 | const json = JSON.parse(buffer);
120 |
121 | // need content
122 | if (!json.hasOwnProperty('content'))
123 | throw new XnbError(`${filename} does not have "content".`);
124 |
125 | // pull reference of content out of data
126 | const content = json.content;
127 | // search the content object for exports to process
128 | const found = search(content, 'export');
129 |
130 | // if we found data to export
131 | if (found) {
132 | // get the key path from found
133 | const keyPath = found.path;
134 | // get the exported buffer from found
135 | const exported = found.value;
136 |
137 | // resolve found content based on key path if empty then its just content
138 | const foundContent = (keyPath.length ? content[keyPath] : content);
139 |
140 | if (exported == undefined)
141 | throw new XnbError('Invalid file export!');
142 |
143 | // form the path for the exported file
144 | const exportedPath = path.join(dirname, exported);
145 | // load in the exported file
146 | const exportedFile = fs.readFileSync(exportedPath);
147 | // get the extention of the file
148 | const ext = path.extname(exportedPath);
149 |
150 | // switch over supported file extension types
151 | switch (ext) {
152 | // Texture2D to PNG
153 | case '.png':
154 | // get the png data
155 | const png = fromPNG(exportedFile);
156 | // change the exported contents
157 | const data = {
158 | data: png.data,
159 | width: png.width,
160 | height: png.height
161 | };
162 |
163 | if (keyPath.length)
164 | json['content'][keyPath]['export'] = data;
165 | else
166 | json['content']['export'] = data;
167 | break;
168 |
169 | // Compiled Effects
170 | case '.cso':
171 | json['content'] = {
172 | type: 'Effect',
173 | data: exportedFile
174 | }
175 | break;
176 |
177 | // TBin Map
178 | case '.tbin':
179 | json['content'] = {
180 | type: 'TBin',
181 | data: exportedFile
182 | }
183 | break;
184 |
185 | // BmFont Xml
186 | case '.xml':
187 | json['content'] = {
188 | type: 'BmFont',
189 | data: exportedFile.toString()
190 | }
191 | break;
192 | }
193 | }
194 |
195 | // return the JSON
196 | return json;
197 | }
198 |
199 | exports.resolveImports = resolveImports;
200 |
201 | /**
202 | * Search an object for a given key.
203 | * @param {object} object
204 | * @param {String} key
205 | * @param {String[]} path
206 | * @returns {object}
207 | */
208 | const search = (object, key, path = []) => {
209 | // ensure object is defined and is an object
210 | if (!object || typeof object != 'object')
211 | return;
212 |
213 | // if property exists then return it
214 | if (object.hasOwnProperty(key))
215 | return { path, value: object[key] };
216 |
217 | // search the objects for keys
218 | for (let [k, v] of entries(object)) {
219 | if (typeof v == 'object') {
220 | path.push(k);
221 | return search(v, key, path);
222 | }
223 | }
224 |
225 | // didn't find anything
226 | return null;
227 | }
228 |
229 | /**
230 | * Generator for key value pair of object.
231 | * @param {objec} object
232 | * @returns {Generator}
233 | */
234 | function* entries(object) {
235 | for (let key of Object.keys(object))
236 | yield [key, object[key]];
237 | }
238 |
239 | /**
240 | * Converts Texture2D into PNG
241 | * @param {Number} width
242 | * @param {Number} height
243 | * @param {Buffer} buffer
244 | * @returns {Buffer}
245 | */
246 | const toPNG = (width, height, buffer) => {
247 | // create an instance of PNG
248 | const png = new PNG({ width, height, inputHasAlpha: true });
249 | // set the data to the buffer
250 | png.data = buffer
251 |
252 | // return the PNG buffer
253 | return PNG.sync.write(png);
254 | }
255 |
256 | /**
257 | * Converts PNG to Texture2D
258 | * @param {Buffer} data
259 | * @returns {Object}
260 | */
261 | const fromPNG = data => {
262 | const png = PNG.sync.read(data);
263 | return {
264 | data: png.data,
265 | width: png.width,
266 | height: png.height
267 | };
268 | }
269 |
--------------------------------------------------------------------------------
/app/Xnb/index.js:
--------------------------------------------------------------------------------
1 | const BufferReader = require('../BufferReader');
2 | const BufferWriter = require('../BufferWriter');
3 | const Log = require('../Log');
4 | const XnbError = require('../XnbError');
5 |
6 | const { simplifyType, getReader } = require('./TypeReader');
7 | const { StringReader } = require('./Readers');
8 | const ReaderResolver = require('./ReaderResolver');
9 | const Presser = require('../Presser');
10 | const { resolveImport } = require('../Porter');
11 | const LZ4 = require('lz4');
12 |
13 | // "constants" for this class
14 | const HIDEF_MASK = 0x1;
15 | const COMPRESSED_LZ4_MASK = 0x40;
16 | const COMPRESSED_LZX_MASK = 0x80;
17 | const XNB_COMPRESSED_PROLOGUE_SIZE = 14;
18 |
19 | /**
20 | * XNB file class used to read and write XNB files
21 | * @class
22 | * @public
23 | */
24 | class Xnb {
25 |
26 | /**
27 | * Creates new instance of Xnb class
28 | * @constructor
29 | */
30 | constructor() {
31 | // target platform
32 | this.target = '';
33 | // format version
34 | this.formatVersion = 0;
35 | // HiDef flag
36 | this.hidef = false;
37 | // Compressed flag
38 | this.compressed = false;
39 | // compression type
40 | this.compressionType = 0;
41 | // the XNB buffer reader
42 | this.buffer = null;
43 | // the file size
44 | this.fileSize = 0;
45 |
46 | /**
47 | * Array of readers that are used by the XNB file.
48 | * @type {BaseReader[]}
49 | */
50 | this.readers = [];
51 |
52 | /**
53 | * Array of shared resources
54 | * @type {Array}
55 | */
56 | this.sharedResources = [];
57 | }
58 |
59 | /**
60 | * Loads a file into the XNB class.
61 | * @param {String} filename The XNB file you want to load.
62 | */
63 | load(filename) {
64 | Log.info(`Reading file "${filename}" ...`);
65 |
66 | // create a new instance of reader
67 | this.buffer = new BufferReader(filename);
68 |
69 | // validate the XNB file header
70 | this._validateHeader();
71 |
72 | // we validated the file successfully
73 | Log.info('XNB file validated successfully!');
74 |
75 | // read the file size
76 | this.fileSize = this.buffer.readUInt32();
77 |
78 | // verify the size
79 | if (this.buffer.size != this.fileSize)
80 | throw new XnbError('XNB file has been truncated!');
81 |
82 | // print out the file size
83 | Log.debug(`File size: ${this.fileSize} bytes.`);
84 |
85 | // if the file is compressed then we need to decompress it
86 | if (this.compressed) {
87 | // get the decompressed size
88 | const decompressedSize = this.buffer.readUInt32();
89 | Log.debug(`Uncompressed size: ${decompressedSize} bytes.`);
90 |
91 | // decompress LZX format
92 | if (this.compressionType == COMPRESSED_LZX_MASK) {
93 | // get the amount of data to compress
94 | const compressedTodo = this.fileSize - XNB_COMPRESSED_PROLOGUE_SIZE;
95 | // decompress the buffer based on the file size
96 | const decompressed = Presser.decompress(this.buffer, compressedTodo, decompressedSize);
97 | // copy the decompressed buffer into the file buffer
98 | this.buffer.copyFrom(decompressed, XNB_COMPRESSED_PROLOGUE_SIZE, 0, decompressedSize);
99 | // reset the byte seek head to read content
100 | this.buffer.bytePosition = XNB_COMPRESSED_PROLOGUE_SIZE;
101 | }
102 | // decompress LZ4 format
103 | else if (this.compressionType == COMPRESSED_LZ4_MASK) {
104 | // decompressed buffer
105 | const decompressed = Buffer.alloc(decompressedSize);
106 | // allocate buffer for LZ4 decode
107 | let trimmed = this.buffer.buffer.slice(XNB_COMPRESSED_PROLOGUE_SIZE);
108 | // decode the trimmed buffer into decompressed buffer
109 | LZ4.decodeBlock(trimmed, decompressed);
110 | // copy the decompressed buffer into our buffer
111 | this.buffer.copyFrom(decompressed, XNB_COMPRESSED_PROLOGUE_SIZE, 0, decompressedSize);
112 | // reset the byte seek head to read content
113 | this.buffer.bytePosition = XNB_COMPRESSED_PROLOGUE_SIZE;
114 | }
115 | }
116 |
117 | Log.debug(`Reading from byte position: ${this.buffer.bytePosition}`);
118 |
119 | // NOTE: assuming the buffer is now decompressed
120 |
121 | // get the 7-bit value for readers
122 | let count = this.buffer.read7BitNumber();
123 | // log how many readers there are
124 | Log.debug(`Readers: ${count}`);
125 |
126 | // create an instance of string reader
127 | const stringReader = new StringReader();
128 |
129 | // a local copy of readers for the export
130 | const readers = [];
131 |
132 | // loop over the number of readers we have
133 | for (let i = 0; i < count; i++) {
134 | // read the type
135 | const type = stringReader.read(this.buffer);
136 | // read the version
137 | const version = this.buffer.readInt32();
138 |
139 | // get the reader for this type
140 | const simpleType = simplifyType(type);
141 | const reader = getReader(simpleType);
142 |
143 | // add reader to the list
144 | this.readers.push(reader);
145 | // add local reader
146 | readers.push({ type, version });
147 | }
148 |
149 | // get the 7-bit value for shared resources
150 | const shared = this.buffer.read7BitNumber();
151 |
152 | // log the shared resources count
153 | Log.debug(`Shared Resources: ${shared}`);
154 |
155 | // don't accept shared resources since SDV XNB files don't have any
156 | if (shared != 0)
157 | throw new XnbError(`Unexpected (${shared}) shared resources.`);
158 |
159 | // create content reader from the readers loaded
160 | const content = new ReaderResolver(this.readers);
161 | // read the content in
162 | const result = content.read(this.buffer);
163 |
164 | // we loaded the XNB file successfully
165 | Log.info('Successfuly read XNB file!');
166 |
167 | // return the loaded XNB object
168 | return {
169 | header: {
170 | target: this.target,
171 | formatVersion: this.formatVersion,
172 | hidef: this.hidef,
173 | compressed: this.compressed
174 | },
175 | readers,
176 | content: result
177 | };
178 | }
179 |
180 | /**
181 | * Converts JSON into XNB file structure
182 | * @param {Object} json The JSON to convert into a XNB file
183 | */
184 | convert(json) {
185 | // the output buffer for this file
186 | const buffer = new BufferWriter();
187 |
188 | // create an instance of string reader
189 | const stringReader = new StringReader();
190 |
191 | // catch exceptions for invalid JSON file formats
192 | try {
193 | // set the header information
194 | this.target = json.header.target;
195 | this.formatVersion = json.header.formatVersion;
196 | this.hidef = json.header.hidef;
197 | const lz4Compression = (this.target == 'a' || this.target == 'i');
198 | this.compressed = lz4Compression ? true : false; // support android LZ4 compression
199 |
200 | // write the header into the buffer
201 | buffer.write("XNB");
202 | buffer.write(this.target);
203 | buffer.writeByte(this.formatVersion);
204 | // write the LZ4 mask for android compression only
205 | buffer.writeByte(this.hidef | ((this.compressed && lz4Compression) ? COMPRESSED_LZ4_MASK : 0));
206 |
207 | // write temporary filesize
208 | buffer.writeUInt32(0);
209 |
210 | // write the decompression size temporarily if android
211 | if (lz4Compression)
212 | buffer.writeUInt32(0);
213 |
214 | // write the amount of readers
215 | buffer.write7BitNumber(json.readers.length);
216 |
217 | // loop over the readers and load the types
218 | for (let reader of json.readers) {
219 | this.readers.push(getReader(simplifyType(reader.type))); // simplyify the type then get the reader of it
220 | stringReader.write(buffer, reader.type);
221 | buffer.writeUInt32(reader.version);
222 | }
223 |
224 | // write 0 shared resources
225 | buffer.write7BitNumber(0);
226 |
227 | // create reader resolver for content and write it
228 | const content = new ReaderResolver(this.readers);
229 |
230 | // write the content to the reader resolver
231 | content.write(buffer, json.content);
232 |
233 | // trim excess space in the buffer
234 | // NOTE: this buffer allocates default with 500 bytes
235 | buffer.trim();
236 |
237 | // LZ4 compression
238 | if (lz4Compression) {
239 | // create buffer with just the content
240 | const contentBuffer = Buffer.alloc(buffer.bytePosition - XNB_COMPRESSED_PROLOGUE_SIZE);
241 | // copy the content from the main buffer into the content buffer
242 | buffer.buffer.copy(contentBuffer, 0, XNB_COMPRESSED_PROLOGUE_SIZE);
243 |
244 | // create a buffer for the compressed data
245 | let compressed = Buffer.alloc(LZ4.encodeBound(contentBuffer.length));
246 |
247 | // compress the data into the buffer
248 | const compressedSize = LZ4.encodeBlock(contentBuffer, compressed);
249 |
250 | // slice off anything extra
251 | compressed = compressed.slice(0, compressedSize);
252 |
253 | // write the decompressed size into the buffer
254 | buffer.buffer.writeUInt32LE(contentBuffer.length, 10);
255 | // write the file size into the buffer
256 | buffer.buffer.writeUInt32LE(XNB_COMPRESSED_PROLOGUE_SIZE + compressedSize, 6);
257 |
258 | // create a new return buffer
259 | let returnBuffer = Buffer.from(buffer.buffer);
260 |
261 | // splice in the content into the return buffer
262 | compressed.copy(returnBuffer, XNB_COMPRESSED_PROLOGUE_SIZE, 0);
263 |
264 | // slice off the excess
265 | returnBuffer = returnBuffer.slice(0, XNB_COMPRESSED_PROLOGUE_SIZE + compressedSize);
266 |
267 | // return the buffer
268 | return returnBuffer;
269 | }
270 |
271 | // write the file size into the buffer
272 | buffer.buffer.writeUInt32LE(buffer.bytePosition, 6)
273 |
274 | // return the buffer
275 | return buffer.buffer;
276 |
277 | }
278 | catch (ex) {
279 | console.log(ex);
280 | }
281 | }
282 |
283 | /**
284 | * Ensures the XNB file header is valid.
285 | * @private
286 | * @method _validateHeader
287 | */
288 | _validateHeader() {
289 | // ensure buffer isn't null
290 | if (this.buffer == null)
291 | throw new XnbError('Buffer is null');
292 |
293 | // get the magic from the beginning of the file
294 | const magic = this.buffer.readString(3);
295 | // check to see if the magic is correct
296 | if (magic != 'XNB')
297 | throw new XnbError(`Invalid file magic found, expecting "XNB", found "${magic}"`);
298 |
299 | // debug print that valid XNB magic was found
300 | Log.debug('Valid XNB magic found!');
301 |
302 | // load the target platform
303 | this.target = this.buffer.readString(1).toLowerCase();
304 |
305 | // read the target platform
306 | switch (this.target) {
307 | case 'w':
308 | Log.debug('Target platform: Microsoft Windows');
309 | break;
310 | case 'm':
311 | Log.debug('Target platform: Windows Phone 7');
312 | break;
313 | case 'x':
314 | Log.debug('Target platform: Xbox 360');
315 | break;
316 | case 'a':
317 | Log.debug('Target platform: Android');
318 | break;
319 | case 'i':
320 | Log.debug('Target platform: iOS');
321 | break;
322 | default:
323 | Log.warn(`Invalid target platform "${this.target}" found.`);
324 | break;
325 | }
326 |
327 | // read the format version
328 | this.formatVersion = this.buffer.readByte();
329 |
330 | // read the XNB format version
331 | switch (this.formatVersion) {
332 | case 0x3:
333 | Log.debug('XNB Format Version: XNA Game Studio 3.0');
334 | break;
335 | case 0x4:
336 | Log.debug('XNB Format Version: XNA Game Studio 3.1');
337 | break;
338 | case 0x5:
339 | Log.debug('XNB Format Version: XNA Game Studio 4.0');
340 | break;
341 | default:
342 | Log.warn(`XNB Format Version ${Log.h(this.formatVersion)} unknown.`);
343 | break;
344 | }
345 |
346 | // read the flag bits
347 | const flags = this.buffer.readByte(1);
348 | // get the HiDef flag
349 | this.hidef = (flags & HIDEF_MASK) != 0;
350 | // get the compressed flag
351 | this.compressed = (flags & COMPRESSED_LZX_MASK) || (flags & COMPRESSED_LZ4_MASK) != 0;
352 | // set the compression type
353 | // NOTE: probably a better way to do both lines but sticking with this for now
354 | this.compressionType = (flags & COMPRESSED_LZX_MASK) != 0 ? COMPRESSED_LZX_MASK : ((flags & COMPRESSED_LZ4_MASK) ? COMPRESSED_LZ4_MASK : 0);
355 | // debug content information
356 | Log.debug(`Content: ${(this.hidef ? 'HiDef' : 'Reach')}`);
357 | // log compressed state
358 | Log.debug(`Compressed: ${this.compressed}, ${this.compressionType == COMPRESSED_LZX_MASK ? 'LZX' : 'LZ4'}`);
359 | }
360 |
361 | }
362 |
363 | module.exports = Xnb;
364 |
--------------------------------------------------------------------------------
/app/BufferReader.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const Log = require('./Log');
3 | const XnbError = require('./XnbError');
4 | const chalk = require('chalk');
5 |
6 | const LITTLE_ENDIAN = 0;
7 | const BIG_ENDIAN = 1;
8 |
9 | class BufferReader {
10 |
11 | /**
12 | * Creates instance of Reader class.
13 | * @constructor
14 | * @param {String} filename The filename to read with the reader.
15 | */
16 | constructor(filename, endianus = LITTLE_ENDIAN) {
17 | // ensure the file exists
18 | if (!fs.existsSync(filename))
19 | throw new XnbError(`"${filename}" does not exist!`);
20 |
21 | /**
22 | * Sets the endianness of the buffer stream
23 | * @private
24 | * @type {Number}
25 | */
26 | this._endianus = endianus;
27 |
28 | /**
29 | * internal buffer for the reader
30 | * @private
31 | * @type {Buffer}
32 | */
33 | this._buffer = fs.readFileSync(filename);
34 |
35 | /**
36 | * Seek index for the internal buffer.
37 | * @private
38 | * @type {Number}
39 | */
40 | this._offset = 0;
41 |
42 | /**
43 | * Bit offset for bit reading.
44 | * @private
45 | * @type {Number}
46 | */
47 | this._bitOffset = 0;
48 |
49 | /**
50 | * Last debug location for logging byte locations
51 | * @private
52 | * @type {Number}
53 | */
54 | this._lastDebugLoc = 0;
55 | }
56 |
57 | /**
58 | * Seeks to a specific index in the buffer.
59 | * @public
60 | * @param {Number} index Sets the buffer seek index.
61 | * @param {Number} origin Location to seek from
62 | */
63 | seek(index, origin = this._offset) {
64 | const offset = this._offset;
65 | this._offset = Math.max(origin + Number.parseInt(index), 0);
66 | if (this._offset < 0 || this._offset > this.buffer.length)
67 | throw new XnbError(`Buffer seek out of bounds! ${this._offset} ${this.buffer.length}`);
68 | return this._offset - offset;
69 | }
70 |
71 | /**
72 | * Gets the seek index of the buffer.
73 | * @public
74 | * @property bytePosition
75 | * @return {Number} Reurns the buffer seek index.
76 | */
77 | get bytePosition() {
78 | return Number.parseInt(this._offset);
79 | }
80 |
81 | /**
82 | * Sets the seek index of the buffer.
83 | * @public
84 | * @property bytePosition
85 | * @param {Number} value
86 | */
87 | set bytePosition(value) {
88 | this._offset = value;
89 | }
90 |
91 | /**
92 | * Gets the current position for bit reading.
93 | * @public
94 | * @property _bitPosition
95 | * @returns {Number}
96 | */
97 | get bitPosition() {
98 | return Number.parseInt(this._bitOffset);
99 | }
100 |
101 | /**
102 | * Sets the bit position clamped at 16-bit frames
103 | * @public
104 | * @property bitPosition
105 | * @param {Number} offset
106 | */
107 | set bitPosition(offset) {
108 | // when rewinding, reset it back to
109 | if (offset < 0) offset = 16 - offset;
110 | // set the offset and clamp to 16-bit frame
111 | this._bitOffset = offset % 16;
112 | // get byte seek for bit ranges that wrap past 16-bit frames
113 | const byteSeek = ((offset - (Math.abs(offset) % 16)) / 16) * 2;
114 | // seek ahead for overflow on 16-bit frames
115 | this.seek(byteSeek);
116 | }
117 |
118 | /**
119 | * Get the buffer size.
120 | * @public
121 | * @property size
122 | * @return {Number} Returns the size of the buffer.
123 | */
124 | get size() {
125 | return this.buffer.length;
126 | }
127 |
128 | /**
129 | * Returns the buffer.
130 | * @public
131 | * @property buffer
132 | * @returns {Buffer} Returns the internal buffer.
133 | */
134 | get buffer() {
135 | return this._buffer;
136 | }
137 |
138 | /**
139 | * Writes another buffer into this buffer.
140 | * @public
141 | * @method write
142 | * @param {Buffer} buffer
143 | * @param {Number} targetIndex
144 | * @param {Number} sourceIndex
145 | * @param {Number} length
146 | */
147 | copyFrom(buffer, targetIndex = 0, sourceIndex = 0, length = buffer.length) {
148 | // we need to resize the buffer to fit the contents
149 | if (this.buffer.length < length + targetIndex) {
150 | // create a temporary buffer of the new size
151 | const tempBuffer = Buffer.alloc(this.buffer.length + (length + targetIndex - this.buffer.length));
152 | // copy our buffer into the temp buffer
153 | this.buffer.copy(tempBuffer);
154 | // copy the buffer given into the temp buffer
155 | buffer.copy(tempBuffer, targetIndex, sourceIndex, length);
156 | // assign our buffer to the temporary buffer
157 | this._buffer = tempBuffer;
158 | }
159 | else {
160 | // copy the buffer into our buffer
161 | buffer.copy(this.buffer, targetIndex, sourceIndex, length);
162 | }
163 | }
164 |
165 | /**
166 | * Reads a specific number of bytes.
167 | * @public
168 | * @method read
169 | * @param {Number} count Number of bytes to read.
170 | * @returns {Buffer} Contents of the buffer.
171 | */
172 | read(count) {
173 | // read from the buffer
174 | const buffer = this.buffer.slice(this._offset, this._offset + count);
175 | // advance seek offset
176 | this.seek(count);
177 | // debug this read
178 | //if (this._debug_mode) this.debug();
179 | // return the read buffer
180 | return buffer;
181 | }
182 |
183 | /**
184 | * Reads a single byte
185 | * @public
186 | * @returns {Number}
187 | */
188 | readByte() {
189 | return this.readUInt();
190 | }
191 |
192 | /**
193 | * Reads an int8
194 | * @public
195 | * @returns {Number}
196 | */
197 | readInt() {
198 | return this.read(1).readInt8();
199 | }
200 |
201 | /**
202 | * Reads an uint8
203 | * @public
204 | * @returns {Number}
205 | */
206 | readUInt() {
207 | return this.read(1).readUInt8();
208 | }
209 |
210 | /**
211 | * Reads a uint16
212 | * @public
213 | * @returns {Number}
214 | */
215 | readUInt16() {
216 | const read = this.read(2);
217 | if (this._endianus == LITTLE_ENDIAN)
218 | return read.readUInt16LE();
219 | return read.readUInt16BE();
220 | }
221 |
222 | /**
223 | * Reads a uint32
224 | * @public
225 | * @returns {Number}
226 | */
227 | readUInt32() {
228 | const read = this.read(4);
229 | if (this._endianus == LITTLE_ENDIAN)
230 | return read.readUInt32LE();
231 | return read.readUInt32BE();
232 | }
233 |
234 | /**
235 | * Reads an int16
236 | * @public
237 | * @returns {Number}
238 | */
239 | readInt16() {
240 | const read = this.read(2);
241 | if (this._endianus == LITTLE_ENDIAN)
242 | return read.readInt16LE();
243 | return read.readInt16BE();
244 | }
245 |
246 | /**
247 | * Reads an int32
248 | * @public
249 | * @returns {Number}
250 | */
251 | readInt32() {
252 | const read = this.read(4);
253 | if (this._endianus == LITTLE_ENDIAN)
254 | return read.readInt32LE();
255 | return read.readInt32BE();
256 | }
257 |
258 | /**
259 | * Reads a float
260 | * @public
261 | * @returns {Number}
262 | */
263 | readSingle() {
264 | const read = this.read(4);
265 | if (this._endianus == LITTLE_ENDIAN)
266 | return read.readFloatLE();
267 | return read.readFloatBE();
268 | }
269 |
270 | /**
271 | * Reads a double
272 | * @public
273 | * @returns {Number}
274 | */
275 | readDouble() {
276 | const read = this.read(4);
277 | if (this._endianus == LITTLE_ENDIAN)
278 | return read.readDoubleLE();
279 | return read.readDoubleBE();
280 | }
281 |
282 | /**
283 | * Reads a string
284 | * @public
285 | * @param {Number} [count]
286 | * @returns {String}
287 | */
288 | readString(count = 0) {
289 | if (count === 0) {
290 | const chars = [];
291 | while (this.peekByte(1) != 0x0)
292 | chars.push(this.readString(1));
293 | this.seek(1);
294 | return chars.join('');
295 | }
296 | return this.read(count).toString();
297 | }
298 |
299 | /**
300 | * Peeks ahead in the buffer without actually seeking ahead.
301 | * @public
302 | * @method peek
303 | * @param {Number} count Number of bytes to peek.
304 | * @returns {Buffer} Contents of the buffer.
305 | */
306 | peek(count) {
307 | // read from the buffer
308 | const buffer = this.read(count);
309 | // rewind the buffer
310 | this.seek(-count);
311 | // return the buffer
312 | return buffer;
313 | }
314 |
315 | /**
316 | * Peeks a single byte
317 | * @public
318 | * @returns {Number}
319 | */
320 | peekByte() {
321 | return this.peekUInt();
322 | }
323 |
324 | /**
325 | * Peeks an int8
326 | * @public
327 | * @returns {Number}
328 | */
329 | peekInt() {
330 | return this.peek(1).readInt8();
331 | }
332 |
333 | /**
334 | * Peeks an uint8
335 | * @public
336 | * @returns {Number}
337 | */
338 | peekUInt() {
339 | return this.peek(1).readUInt8();
340 | }
341 |
342 | /**
343 | * Peeks a uint16
344 | * @public
345 | * @returns {Number}
346 | */
347 | peekUInt16() {
348 | if (this._endianus == LITTLE_ENDIAN)
349 | return this.peek(2).readUInt16LE();
350 | return this.peek(2).readUInt16BE();
351 | }
352 |
353 | /**
354 | * Peeks a uint32
355 | * @public
356 | * @returns {Number}
357 | */
358 | peekUInt32() {
359 | if (this._endianus == LITTLE_ENDIAN)
360 | return this.peek(4).readUInt32LE();
361 | return this.peek(4).readUInt32BE();
362 | }
363 |
364 | /**
365 | * Peeks an int16
366 | * @public
367 | * @returns {Number}
368 | */
369 | peekInt16() {
370 | if (this._endianus == LITTLE_ENDIAN)
371 | return this.peek(2).readInt16LE();
372 | return this.peek(2).readInt16BE();
373 | }
374 |
375 | /**
376 | * Peeks an int32
377 | * @public
378 | * @returns {Number}
379 | */
380 | peekInt32() {
381 | if (this._endianus == LITTLE_ENDIAN)
382 | return this.peek(4).readInt32LE();
383 | return this.peek(4).readInt32BE();
384 | }
385 |
386 | /**
387 | * Peeks a float
388 | * @public
389 | * @returns {Number}
390 | */
391 | peekSingle() {
392 | if (this._endianus == LITTLE_ENDIAN)
393 | return this.peek(4).readFloatLE();
394 | return this.peek(4).readFloatBE();
395 | }
396 |
397 | /**
398 | * Peeks a double
399 | * @public
400 | * @returns {Number}
401 | */
402 | peekDouble() {
403 | if (this._endianus == LITTLE_ENDIAN)
404 | return this.peek(4).readDoubleLE();
405 | return this.peek(4).readDoubleBE();
406 | }
407 |
408 | /**
409 | * Peeks a string
410 | * @public
411 | * @param {Number} [count]
412 | * @returns {String}
413 | */
414 | peekString(count = 0) {
415 | if (count === 0) {
416 | const bytePosition = this.bytePosition;
417 | const chars = [];
418 | while (this.peekByte(1) != 0x0)
419 | chars.push(this.readString(1));
420 | this.bytePosition = bytePosition;
421 | return str.join('');
422 | }
423 | return this.peek(count).toString();
424 | }
425 |
426 | /**
427 | * Reads a 7-bit number.
428 | * @public
429 | * @method read7BitNumber
430 | * @returns {Number} Returns the number read.
431 | */
432 | read7BitNumber() {
433 | let result = 0;
434 | let bitsRead = 0;
435 | let value;
436 |
437 | // loop over bits
438 | do {
439 | value = this.readByte();
440 | result |= (value & 0x7F) << bitsRead;
441 | bitsRead += 7;
442 | }
443 | while (value & 0x80);
444 |
445 | return result;
446 | }
447 |
448 | /**
449 | * Reads bits used for LZX compression.
450 | * @public
451 | * @method readLZXBits
452 | * @param {Number} bits
453 | * @returns {Number}
454 | */
455 | readLZXBits(bits) {
456 | // initialize values for the loop
457 | let bitsLeft = bits;
458 | let read = 0;
459 |
460 | // read bits in 16-bit chunks
461 | while (bitsLeft > 0) {
462 | // peek in a 16-bit value
463 | const peek = this.peek(2).readUInt16LE();
464 |
465 | // clamp bits into the 16-bit frame we have left only read in as much as we have left
466 | const bitsInFrame = Math.min(Math.max(bitsLeft, 0), 16 - this.bitPosition);
467 | // set the offset based on current position in and bit count
468 | const offset = 16 - this.bitPosition - bitsInFrame;
469 |
470 | // create mask and shift the mask up to the offset <<
471 | // and then shift the return back down into mask space >>
472 | const value = (peek & (2 ** bitsInFrame - 1 << offset)) >> offset;
473 |
474 | // Log.debug(Log.b(peek, 16, this.bitPosition, this.bitPosition + bitsInFrame));
475 |
476 | // remove the bits we read from what we have left
477 | bitsLeft -= bitsInFrame;
478 | // add the bits read to the bit position
479 | this.bitPosition += bitsInFrame;
480 |
481 | // assign read with the value shifted over for reading in loops
482 | read |= value << bitsLeft;
483 | }
484 |
485 | // return the read bits
486 | return read;
487 | }
488 |
489 | /**
490 | * Used to peek bits.
491 | * @public
492 | * @method peekLZXBits
493 | * @param {Number} bits
494 | * @returns {Number}
495 | */
496 | peekLZXBits(bits) {
497 | // get the current bit position to store
498 | let bitPosition = this.bitPosition;
499 | // get the current byte position to store
500 | let bytePosition = this.bytePosition;
501 |
502 | // read the bits like normal
503 | const read = this.readLZXBits(bits);
504 |
505 | // just rewind the bit position, this will also rewind bytes where needed
506 | this.bitPosition = bitPosition;
507 | // restore the byte position
508 | this.bytePosition = bytePosition;
509 |
510 | // return the peeked value
511 | return read;
512 | }
513 |
514 | /**
515 | * Reads a 16-bit integer from a LZX bitstream
516 | *
517 | * bytes are reverse as the bitstream sequences 16 bit integers stored as LSB -> MSB (bytes)
518 | * abc[...]xyzABCDEF as bits would be stored as:
519 | * [ijklmnop][abcdefgh][yzABCDEF][qrstuvwx]
520 | *
521 | * @public
522 | * @method readLZXInt16
523 | * @param {Boolean} seek
524 | * @returns {Number}
525 | */
526 | readLZXInt16(seek = true) {
527 | // read in the next two bytes worth of data
528 | const lsB = this.readByte();
529 | const msB = this.readByte();
530 |
531 | // rewind the seek head
532 | if (!seek)
533 | this.seek(-2);
534 |
535 | // set the value
536 | return (lsB << 8) | msB;
537 | }
538 |
539 | /**
540 | * Aligns to 16-bit offset.
541 | * @public
542 | * @method align
543 | */
544 | align() {
545 | if (this.bitPosition > 0)
546 | this.bitPosition += 16 - this.bitPosition;
547 | }
548 |
549 | /**
550 | * Used only for error logging.
551 | * @public
552 | */
553 | debug() {
554 | // store reference to the byte position
555 | const bytePosition = this.bytePosition;
556 | // move back by 8 bytes
557 | const diff = Math.abs(this.seek(-8));
558 | // read 16 bytes worth of data into an array
559 | const read = this.peek(17).values();
560 | const bytes = [];
561 | const chars = [];
562 | let i = 0;
563 | for (let byte of read) {
564 | bytes.push('00'.slice(0, 2 - byte.toString(16).length) + byte.toString(16).toUpperCase());
565 | let char;
566 | if (byte > 0x1f && byte < 0x7E)
567 | char = String.fromCharCode(byte);
568 | else
569 | char = ' ';
570 | chars.push(char);
571 | i++;
572 | }
573 | const ldlpos = diff - (bytePosition - this._lastDebugLoc);
574 | // replace the selected byte with brackets
575 | bytes[diff] = chalk.black.bgBlue(bytes[diff]);
576 | if (ldlpos > 0 && ldlpos < 16)
577 | bytes[ldlpos] = chalk.black.bgMagenta(bytes[ldlpos]);
578 |
579 | // log the message
580 | console.log(bytes.join(' '));
581 | console.log(chalk.gray(chars.join(' ')));
582 |
583 | // re-seek back
584 | this.seek(bytePosition, 0);
585 | // update last debug loc
586 | this._lastDebugLoc = bytePosition;
587 | }
588 | }
589 |
590 | // export the BufferReader class
591 | module.exports = BufferReader;
592 |
--------------------------------------------------------------------------------
/app/Presser/Lzx.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This file is heavily based on MonoGame's implementation of their LzxDecoder attributed to Ali Scissons
4 | * which is derived from libmspack by Stuart Cole.
5 | *
6 | * (C) 2003-2004 Stuart Caie.
7 | * (C) 2011 Ali Scissons.
8 | * (C) 2017 James Stine.
9 | *
10 | * The LZX method was created by Johnathan Forbes and Tomi Poutanen, adapted by Microsoft Corporation.
11 | *
12 | */
13 |
14 | /**
15 | * GNU LESSER GENERAL PUBLIC LICENSE version 2.1
16 | * LzxDecoder is free software; you can redistribute it and/or modify it under
17 | * the terms of the GNU Lesser General Public License (LGPL) version 2.1
18 | */
19 |
20 | /**
21 | * MICROSOFT PUBLIC LICENSE
22 | * This source code a derivative on LzxDecoder and is subject to the terms of the Microsoft Public License (Ms-PL).
23 | *
24 | * Redistribution and use in source and binary forms, with or without modification,
25 | * is permitted provided that redistributions of the source code retain the above
26 | * copyright notices and this file header.
27 | *
28 | * Additional copyright notices should be appended to the list above.
29 | *
30 | * For details, see .
31 | *
32 | */
33 |
34 | /**
35 | * I made the mistake of not including this license years ago. Big thanks to everyone involved and license has now been
36 | * acknowleded properly as it should have been back in 2017.
37 | *
38 | * Resources:
39 | *
40 | * cabextract/libmspack - http://http://www.cabextract.org.uk/
41 | * MonoGame LzxDecoder.cs - https://github.com/MonoGame/MonoGame/blob/master/MonoGame.Framework/Content/LzxDecoder.cs
42 | *
43 | */
44 |
45 | const Log = require('../Log');
46 | const BufferReader = require('../BufferReader');
47 | const XnbError = require('../XnbError');
48 |
49 | // LZX Constants
50 | const MIN_MATCH = 2; // smallest allowable match length
51 | const MAX_MATCH = 257; // largest allowable match length
52 | const NUM_CHARS = 256; // number of uncompressed character types
53 | const BLOCKTYPE = {
54 | INVALID: 0,
55 | VERBATIM: 1,
56 | ALIGNED: 2,
57 | UNCOMPRESSED: 3
58 | };
59 | const PRETREE_NUM_ELEMENTS = 20;
60 | const ALIGNED_NUM_ELEMENTS = 8; // aligned offset tree elements
61 | const NUM_PRIMARY_LENGTHS = 7;
62 | const NUM_SECONDARY_LENGTHS = 249; // number of elements in length tree
63 |
64 | // LZX Huffman Constants
65 | const PRETREE_MAXSYMBOLS = PRETREE_NUM_ELEMENTS;
66 | const PRETREE_TABLEBITS = 6;
67 | const MAINTREE_MAXSYMBOLS = NUM_CHARS + 50 * 8;
68 | const MAINTREE_TABLEBITS = 12;
69 | const LENGTH_MAXSYMBOLS = NUM_SECONDARY_LENGTHS + 1;
70 | const LENGTH_TABLEBITS = 12;
71 | const ALIGNED_MAXSYMBOLS = ALIGNED_NUM_ELEMENTS;
72 | const ALIGNED_TABLEBITS = 7;
73 | const LENTABLE_SAFETY = 64; // table decoding overruns are allowed
74 |
75 | /**
76 | * LZX Static Data Tables
77 | *
78 | * LZX uses 'position slots' to represent match offsets. For every match,
79 | * a small 'position slot' number and a small offset from that slot are
80 | * encoded instead of one large offset.
81 | *
82 | * Lzx.position_base[] is an index to the position slot bases
83 | *
84 | * Lzx.extra_bits[] states how many bits of offset-from-base data is needed.
85 | */
86 |
87 | /**
88 | * Used to compress and decompress LZX format buffer.
89 | * @class
90 | * @public
91 | */
92 | class Lzx {
93 |
94 | /**
95 | * Creates an instance of LZX with a given window frame.
96 | * @constructor
97 | * @param {Number} window_bits
98 | */
99 | constructor(window_bits) {
100 | // get the window size from window bits
101 | this.window_size = 1 << window_bits;
102 |
103 | // LZX supports window sizes of 2^15 (32 KB) to 2^21 (2 MB)
104 | if (window_bits < 15 || window_bits > 21)
105 | throw new XnbError('Window size out of range!');
106 |
107 | // initialize static tables
108 | if (!Lzx.extra_bits.length) {
109 | for (let i = 0, j = 0; i <= 50; i += 2) {
110 | Lzx.extra_bits[i] = Lzx.extra_bits[i + 1] = j;
111 | if (i != 0 && j < 17)
112 | j++;
113 | }
114 | }
115 | if (!Lzx.position_base.length) {
116 | for (let i = 0, j = 0; i <= 50; i++) {
117 | Lzx.position_base[i] = j;
118 | j += 1 << Lzx.extra_bits[i];
119 | }
120 | }
121 |
122 | Log.debug(`Extra Bits:`);
123 | Log.debug(JSON.stringify(Lzx.extra_bits));
124 | Log.debug(`Position Base:`);
125 | Log.debug(JSON.stringify(Lzx.position_base));
126 |
127 | /**
128 | * calculate required position slots
129 | *
130 | * window bits: 15 16 17 18 19 20 21
131 | * position slots: 30 32 34 36 38 42 50
132 | */
133 | const posn_slots = (window_bits == 21 ? 50 : (window_bits == 20 ? 42 : window_bits << 1));
134 |
135 | // repeated offsets
136 | this.R0 = this.R1 = this.R2 = 1;
137 | // set the number of main elements
138 | this.main_elements = NUM_CHARS + (posn_slots << 3);
139 | // state of header being read used for when looping over multiple blocks
140 | this.header_read = false;
141 | // set the block remaining
142 | this.block_remaining = 0;
143 | // set the default block type
144 | this.block_type = BLOCKTYPE.INVALID;
145 | // window position
146 | this.window_posn = 0;
147 |
148 | // frequently used tables
149 | this.pretree_table = [];
150 | this.pretree_len = [];
151 | this.aligned_table = [];
152 | this.aligned_len = [];
153 | this.length_table = [];
154 | this.length_len = [];
155 | this.maintree_table = [];
156 | this.maintree_len = [];
157 |
158 | // initialize main tree and length tree for use with delta operations
159 | for (let i = 0; i < MAINTREE_MAXSYMBOLS; i++)
160 | this.maintree_len[i] = 0;
161 | for (let i = 0; i < NUM_SECONDARY_LENGTHS; i++)
162 | this.length_len[i] = 0;
163 |
164 | // the decompression window
165 | this.win = [];
166 | }
167 |
168 | /**
169 | * Decompress the buffer with given frame and block size
170 | * @param {BufferReader} buffer
171 | * @param {Number} frame_size
172 | * @param {Number} block_size
173 | * @returns {Number[]}
174 | */
175 | decompress(buffer, frame_size, block_size) {
176 |
177 | // read header if we haven't already
178 | if (!this.header_read) {
179 | // read the intel call
180 | const intel = buffer.readLZXBits(1);
181 |
182 | Log.debug(`Intel: ${Log.b(intel, 1)} = ${intel}`);
183 |
184 | // don't care about intel e8
185 | if (intel != 0)
186 | throw new XnbError(`Intel E8 Call found, invalid for XNB files.`);
187 |
188 | // the header has been read
189 | this.header_read = true;
190 | }
191 |
192 | // set what's left to go to the frame size
193 | let togo = frame_size;
194 |
195 | // loop over what's left of the frame
196 | while (togo > 0) {
197 |
198 | // this is a new block
199 | if (this.block_remaining == 0) {
200 | // read in the block type
201 | this.block_type = buffer.readLZXBits(3);
202 |
203 | Log.debug(`Blocktype: ${Log.b(this.block_type, 3)} = ${this.block_type}`);
204 |
205 | // read 24-bit value for uncompressed bytes in this block
206 | const hi = buffer.readLZXBits(16);
207 | const lo = buffer.readLZXBits(8);
208 | // number of uncompressed bytes for this block left
209 | this.block_remaining = (hi << 8) | lo;
210 |
211 | Log.debug(`Block Remaining: ${this.block_remaining}`);
212 |
213 | // switch over the valid block types
214 | switch (this.block_type) {
215 | case BLOCKTYPE.ALIGNED:
216 | // aligned offset tree
217 | for (let i = 0; i < 8; i++)
218 | this.aligned_len[i] = buffer.readLZXBits(3);
219 | // decode table for aligned tree
220 | this.aligned_table = this.decodeTable(
221 | ALIGNED_MAXSYMBOLS,
222 | ALIGNED_TABLEBITS,
223 | this.aligned_len
224 | );
225 | // NOTE: rest of aligned block type is the same as verbatim block type
226 | case BLOCKTYPE.VERBATIM:
227 | // read the first 256 elements for main tree
228 | this.readLengths(buffer, this.maintree_len, 0, 256);
229 | // read the rest of the elements for the main tree
230 | this.readLengths(buffer, this.maintree_len, 256, this.main_elements);
231 | // decode the main tree into a table
232 | this.maintree_table = this.decodeTable(
233 | MAINTREE_MAXSYMBOLS,
234 | MAINTREE_TABLEBITS,
235 | this.maintree_len
236 | );
237 | // read path lengths for the length tree
238 | this.readLengths(buffer, this.length_len, 0, NUM_SECONDARY_LENGTHS);
239 | // decode the length tree
240 | this.length_table = this.decodeTable(
241 | LENGTH_MAXSYMBOLS,
242 | LENGTH_TABLEBITS,
243 | this.length_len
244 | );
245 | break;
246 | case BLOCKTYPE.UNCOMPRESSED:
247 | // align the bit buffer to byte range
248 | buffer.align();
249 | // read the offsets
250 | this.R0 = buffer.readInt32();
251 | this.R1 = buffer.readInt32();
252 | this.R2 = buffer.readInt32();
253 | break;
254 | default:
255 | throw new XnbError(`Invalid Blocktype Found: ${this.block_type}`);
256 | break;
257 | }
258 | }
259 |
260 | // iterate over the block remaining
261 | let this_run = this.block_remaining;
262 |
263 | // loop over the bytes left in the buffer to run out our output
264 | while ((this_run = this.block_remaining) > 0 && togo > 0) {
265 | // if this run is somehow higher than togo then just cap it
266 | if (this_run > togo)
267 | this_run = togo;
268 |
269 | // reduce togo and block remaining by this iteration
270 | togo -= this_run;
271 | this.block_remaining -= this_run;
272 |
273 | // apply 2^x-1 mask
274 | this.window_posn &= this.window_size - 1;
275 | // run cannot exceed frame size
276 | if (this.window_posn + this_run > this.window_size)
277 | throw new XnbError('Cannot run outside of window frame.');
278 |
279 | switch (this.block_type) {
280 | case BLOCKTYPE.ALIGNED:
281 | while (this_run > 0) {
282 | // get the element of this run
283 | let main_element = this.readHuffSymbol(
284 | buffer,
285 | this.maintree_table,
286 | this.maintree_len,
287 | MAINTREE_MAXSYMBOLS,
288 | MAINTREE_TABLEBITS
289 | );
290 |
291 | // main element is an unmatched character
292 | if (main_element < NUM_CHARS) {
293 | this.win[this.window_posn++] = main_element;
294 | this_run--;
295 | continue;
296 | }
297 |
298 | main_element -= NUM_CHARS;
299 |
300 | let length_footer;
301 |
302 | let match_length = main_element & NUM_PRIMARY_LENGTHS;
303 | if (match_length == NUM_PRIMARY_LENGTHS) {
304 | // get the length footer
305 | length_footer = this.readHuffSymbol(
306 | buffer,
307 | this.length_table,
308 | this.length_len,
309 | LENGTH_MAXSYMBOLS,
310 | LENGTH_TABLEBITS
311 | );
312 | // increase match length by the footer
313 | match_length += length_footer;
314 | }
315 | match_length += MIN_MATCH;
316 |
317 | let match_offset = main_element >> 3;
318 |
319 | if (match_offset > 2) {
320 | // not repeated offset
321 | let extra = Lzx.extra_bits[match_offset];
322 | match_offset = Lzx.position_base[match_offset] - 2;
323 | if (extra > 3) {
324 | // verbatim and aligned bits
325 | extra -= 3;
326 | let verbatim_bits = buffer.readLZXBits(extra);
327 | match_offset += verbatim_bits << 3;
328 | let aligned_bits = this.readHuffSymbol(
329 | buffer,
330 | this.aligned_table,
331 | this.aligned_len,
332 | ALIGNED_MAXSYMBOLS,
333 | ALIGNED_TABLEBITS
334 | );
335 | match_offset += aligned_bits;
336 | }
337 | else if (extra == 3) {
338 | // aligned bits only
339 | match_offset += this.readHuffSymbol(
340 | buffer,
341 | this.aligned_table,
342 | this.aligned_len,
343 | ALIGNED_MAXSYMBOLS,
344 | ALIGNED_TABLEBITS
345 | );
346 | }
347 | else if (extra > 0)
348 | // verbatim bits only
349 | match_offset += buffer.readLZXBits(extra);
350 | else
351 | match_offset = 1; // ???
352 |
353 | // update repeated offset LRU queue
354 | this.R2 = this.R1;
355 | this.R1 = this.R0;
356 | this.R0 = match_offset;
357 | }
358 | else if (match_offset === 0) {
359 | match_offset = this.R0;
360 | }
361 | else if (match_offset == 1) {
362 | match_offset = this.R1;
363 | this.R1 = this.R0;
364 | this.R0 = match_offset;
365 | }
366 | else {
367 | match_offset = this.R2;
368 | this.R2 = this.R0;
369 | this.R0 = match_offset;
370 | }
371 |
372 | let rundest = this.window_posn;
373 | let runsrc;
374 | this_run -= match_length;
375 |
376 | // copy any wrapped around source data
377 | if (this.window_posn >= match_offset)
378 | runsrc = rundest - match_offset; // no wrap
379 | else {
380 | runsrc = rundest + (this.window_size - match_offset);
381 | let copy_length = match_offset - this.window_posn;
382 | if (copy_length < match_length) {
383 | match_length -= copy_length;
384 | this.window_posn += copy_length;
385 | while (copy_length-- > 0)
386 | this.win[rundest++] = this.win[runsrc++];
387 | runsrc = 0;
388 | }
389 | }
390 | this.window_posn += match_length;
391 |
392 | // copy match data - no worrries about destination wraps
393 | while (match_length-- > 0)
394 | this.win[rundest++] = this.win[runsrc++];
395 | }
396 | break;
397 |
398 | case BLOCKTYPE.VERBATIM:
399 | while (this_run > 0) {
400 | // get the element of this run
401 | let main_element = this.readHuffSymbol(
402 | buffer,
403 | this.maintree_table,
404 | this.maintree_len,
405 | MAINTREE_MAXSYMBOLS,
406 | MAINTREE_TABLEBITS
407 | );
408 |
409 | // main element is an unmatched character
410 | if (main_element < NUM_CHARS) {
411 | this.win[this.window_posn++] = main_element;
412 | this_run--;
413 | continue;
414 | }
415 |
416 | // match: NUM_CHARS + ((slot << 3) | length_header (3 bits))
417 |
418 | main_element -= NUM_CHARS;
419 |
420 | let length_footer;
421 |
422 | let match_length = main_element & NUM_PRIMARY_LENGTHS;
423 | if (match_length == NUM_PRIMARY_LENGTHS) {
424 | // read the length footer
425 | length_footer = this.readHuffSymbol(
426 | buffer,
427 | this.length_table,
428 | this.length_len,
429 | LENGTH_MAXSYMBOLS,
430 | LENGTH_TABLEBITS
431 | );
432 | match_length += length_footer;
433 | }
434 | match_length += MIN_MATCH;
435 |
436 | let match_offset = main_element >> 3;
437 |
438 | if (match_offset > 2) {
439 | // not repeated offset
440 | if (match_offset != 3) {
441 | let extra = Lzx.extra_bits[match_offset];
442 | let verbatim_bits = buffer.readLZXBits(extra);
443 | match_offset = Lzx.position_base[match_offset] - 2 + verbatim_bits;
444 | }
445 | else
446 | match_offset = 1;
447 |
448 | // update repeated offset LRU queue
449 | this.R2 = this.R1;
450 | this.R1 = this.R0;
451 | this.R0 = match_offset;
452 | }
453 | else if (match_offset === 0) {
454 | match_offset = this.R0;
455 | }
456 | else if (match_offset == 1) {
457 | match_offset = this.R1;
458 | this.R1 = this.R0;
459 | this.R0 = match_offset;
460 | }
461 | else {
462 | match_offset = this.R2;
463 | this.R2 = this.R0;
464 | this.R0 = match_offset;
465 | }
466 |
467 | let rundest = this.window_posn;
468 | let runsrc;
469 | this_run -= match_length;
470 |
471 | // copy any wrapped around source data
472 | if (this.window_posn >= match_offset)
473 | runsrc = rundest - match_offset; // no wrap
474 | else {
475 | runsrc = rundest + (this.window_size - match_offset);
476 | let copy_length = match_offset - this.window_posn;
477 | if (copy_length < match_length) {
478 | match_length -= copy_length;
479 | this.window_posn += copy_length;
480 | while (copy_length-- > 0)
481 | this.win[rundest++] = this.win[runsrc++];
482 | runsrc = 0;
483 | }
484 | }
485 | this.window_posn += match_length;
486 |
487 | // copy match data - no worrries about destination wraps
488 | while (match_length-- > 0)
489 | this.win[rundest++] = this.win[runsrc++];
490 | }
491 | break;
492 |
493 | case BLOCKTYPE.UNCOMPRESSED:
494 | if ((buffer.bytePosition + this_run) > block_size)
495 | throw new XnbError('Overrun!' + block_size + ' ' + buffer.bytePosition + ' ' + this_run);
496 | for (let i = 0; i < this_run; i++)
497 | this.win[window_posn + i] = buffer.buffer[buffer.bytePosition + i];
498 | buffer.bytePosition += this_run;
499 | this.window_posn += this_run;
500 | break;
501 |
502 | default:
503 | throw new XnbError('Invalid blocktype specified!');
504 | }
505 | }
506 | }
507 |
508 | // there is still more left
509 | if (togo != 0)
510 | throw new XnbError('EOF reached with data left to go.');
511 |
512 | // ensure the buffer is aligned
513 | buffer.align();
514 |
515 | // get the start window position
516 | const start_window_pos = ((this.window_posn == 0) ? this.window_size : this.window_posn) - frame_size;
517 |
518 | // return the window
519 | return this.win.slice(start_window_pos, start_window_pos + frame_size);
520 | }
521 |
522 | /**
523 | * Reads in code lengths for symbols first to last in the given table
524 | * The code lengths are stored in their own special LZX way.
525 | * @public
526 | * @method readLengths
527 | * @param {BufferReader} buffer
528 | * @param {Array} table
529 | * @param {Number} first
530 | * @param {Number} last
531 | * @returns {Array}
532 | */
533 | readLengths(buffer, table, first, last) {
534 | // read in the 4-bit pre-tree deltas
535 | for (let i = 0; i < 20; i++)
536 | this.pretree_len[i] = buffer.readLZXBits(4);
537 |
538 | // create pre-tree table from lengths
539 | this.pretree_table = this.decodeTable(
540 | PRETREE_MAXSYMBOLS,
541 | PRETREE_TABLEBITS,
542 | this.pretree_len
543 | );
544 |
545 | // loop through the lengths from first to last
546 | for (let i = first; i < last;) {
547 |
548 | // read in the huffman symbol
549 | let symbol = this.readHuffSymbol(
550 | buffer,
551 | this.pretree_table,
552 | this.pretree_len,
553 | PRETREE_MAXSYMBOLS,
554 | PRETREE_TABLEBITS
555 | );
556 |
557 | // code = 17, run of ([read 4 bits] + 4) zeros
558 | if (symbol == 17) {
559 | // read in number of zeros as a 4-bit number + 4
560 | let zeros = buffer.readLZXBits(4) + 4;
561 | // iterate over zeros counter and add them to the table
562 | while (zeros-- != 0)
563 | table[i++] = 0;
564 | }
565 | // code = 18, run of ([read 5 bits] + 20) zeros
566 | else if (symbol == 18) {
567 | // read in number of zeros as a 5-bit number + 20
568 | let zeros = buffer.readLZXBits(5) + 20;
569 | // add the number of zeros into the table array
570 | while (zeros-- != 0)
571 | table[i++] = 0;
572 | }
573 | // code = 19 run of ([read 1 bit] + 4) [read huffman symbol]
574 | else if (symbol == 19) {
575 | // read for how many of the same huffman symbol to repeat
576 | let same = buffer.readLZXBits(1) + 4;
577 | // read another huffman symbol
578 | symbol = this.readHuffSymbol(
579 | buffer,
580 | this.pretree_table,
581 | this.pretree_len,
582 | PRETREE_MAXSYMBOLS,
583 | PRETREE_TABLEBITS
584 | );
585 | symbol = table[i] - symbol;
586 | if (symbol < 0) symbol += 17;
587 | while (same-- != 0)
588 | table[i++] = symbol;
589 | }
590 | // code 0 -> 16, delta current length entry
591 | else {
592 | symbol = table[i] - symbol;
593 | if (symbol < 0) symbol += 17;
594 | table[i++] = symbol;
595 | }
596 | }
597 |
598 | // return the table created
599 | return table;
600 | }
601 |
602 | /**
603 | * Build a decode table from a canonical huffman lengths table
604 | * @public
605 | * @method makeDecodeTable
606 | * @param {Number} symbols Total number of symbols in tree.
607 | * @param {Number} bits Any symbols less than this can be decoded in one lookup of table.
608 | * @param {Number[]} length Table for lengths of given table to decode.
609 | * @returns {Number[]} Decoded table, length should be ((1<> 1;
618 |
619 | // loop across all bit positions
620 | for (let bit_num = 1; bit_num <= bits; bit_num++) {
621 | // loop over the symbols we're decoding
622 | for (let symbol = 0; symbol < symbols; symbol++) {
623 | // if the symbol isn't in this iteration of length then just ignore
624 | if (length[symbol] == bit_num) {
625 | let leaf = pos;
626 | // if the position has gone past the table mask then we're overrun
627 | if ((pos += bit_mask) > table_mask) {
628 | Log.debug(length[symbol]);
629 | Log.debug(`pos: ${pos}, bit_mask: ${bit_mask}, table_mask: ${table_mask}`);
630 | Log.debug(`bit_num: ${bit_num}, bits: ${bits}`);
631 | Log.debug(`symbol: ${symbol}, symbols: ${symbols}`);
632 | throw new XnbError('Overrun table!');
633 | }
634 | // fill all possible lookups of this symbol with the symbol itself
635 | let fill = bit_mask;
636 | while (fill-- > 0)
637 | table[leaf++] = symbol;
638 | }
639 | }
640 | // advance bit mask down the bit positions
641 | bit_mask >>= 1;
642 | }
643 |
644 | // exit with success if table is complete
645 | if (pos == table_mask)
646 | return table;
647 |
648 | // mark all remaining table entries as unused
649 | for (let symbol = pos; symbol < table_mask; symbol++)
650 | table[symbol] = 0xFFFF;
651 |
652 | // next_symbol = base of allocation for long codes
653 | let next_symbol = ((table_mask >> 1) < symbols) ? symbols : (table_mask >> 1);
654 |
655 | // allocate space for 16-bit values
656 | pos <<= 16;
657 | table_mask <<= 16;
658 | bit_mask = 1 << 15;
659 |
660 | // loop again over the bits
661 | for (let bit_num = bits + 1; bit_num <= 16; bit_num++) {
662 | // loop over the symbol range
663 | for (let symbol = 0; symbol < symbols; symbol++) {
664 | // if the current length iteration doesn't mach our bit then just ignore
665 | if (length[symbol] != bit_num)
666 | continue;
667 |
668 | // get leaf shifted away from 16 bit padding
669 | let leaf = pos >> 16;
670 |
671 | // loop over fill to flood table with
672 | for (let fill = 0; fill < (bit_num - bits); fill++) {
673 | // if this path hasn't been taken yet, 'allocate' two entries
674 | if (table[leaf] == 0xFFFF) {
675 | table[(next_symbol << 1)] = 0xFFFF;
676 | table[(next_symbol << 1) + 1] = 0xFFFF;
677 | table[leaf] = next_symbol++;
678 | }
679 |
680 | // follow the path and select either left or right for the next bit
681 | leaf = table[leaf] << 1;
682 | if ((pos >> (15 - fill)) & 1)
683 | leaf++;
684 | }
685 | table[leaf] = symbol
686 |
687 | // bit position has overun the table mask
688 | if ((pos += bit_mask) > table_mask)
689 | throw new XnbError('Overrun table during decoding.');
690 | }
691 | bit_mask >>= 1;
692 | }
693 |
694 | // we have reached table mask
695 | if (pos == table_mask)
696 | return table;
697 |
698 | // something else went wrong
699 | throw new XnbError('Decode table did not reach table mask.');
700 | }
701 |
702 | /**
703 | * Decodes the next huffman symbol from the bitstream.
704 | * @public
705 | * @method readHuffSymbol
706 | * @param {BufferReader} buffer
707 | * @param {Number[]} table
708 | * @param {Number[]} length
709 | * @param {Number} symbols
710 | * @param {Number} bits
711 | * @returns {Number}
712 | */
713 | readHuffSymbol(buffer, table, length, symbols, bits) {
714 | // peek the specified bits ahead
715 | let bit = (buffer.peekLZXBits(32) >>> 0); // (>>> 0) allows us to get a 32-bit uint
716 | let i = table[buffer.peekLZXBits(bits)];
717 |
718 | // if our table is accessing a symbol beyond our range
719 | if (i >= symbols) {
720 | let j = 1 << (32 - bits);
721 | do {
722 | j >>= 1;
723 | i <<= 1;
724 | i |= (bit & j) != 0 ? 1 : 0;
725 | if (j == 0)
726 | return 0;
727 | }
728 | while ((i = table[i]) >= symbols)
729 | }
730 |
731 | // seek past this many bits
732 | buffer.bitPosition += length[i];
733 |
734 | // return the symbol
735 | return i;
736 | }
737 |
738 | /**
739 | * Sets the shortest match.
740 | * @param {Number} X
741 | */
742 | set RRR(X) {
743 | // No match, R2 <- R1, R1 <- R0, R0 <- X
744 | if (this.R0 != X && this.R1 != X && this.R2 != X) {
745 | // shift all offsets down
746 | this.R2 = this.R1;
747 | this.R1 = this.R0;
748 | this.R0 = X;
749 | }
750 | // X = R1, Swap R0 <-> R1
751 | else if (this.R1 == X) {
752 | let R1 = this.R1;
753 | this.R1 = this.R0;
754 | this.R0 = R1;
755 | }
756 | // X = R2, Swap R0 <-> R2
757 | else if (this.R2 == X) {
758 | let R2 = this.R2;
759 | this.R2 = this.R0;
760 | this.R0 = R2;
761 | }
762 | }
763 | }
764 |
765 | Lzx.position_base = [];
766 | Lzx.extra_bits = [];
767 |
768 | module.exports = Lzx;
769 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | {one line to give the program's name and a brief idea of what it does.}
635 | Copyright (C) {year} {name of author}
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | {project} Copyright (C) {year} {fullname}
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------