diff --git a/package-lock.json b/package-lock.json
index 05adfc6..a6e0c24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "diabloweb",
- "version": "1.0.35",
+ "version": "1.0.36",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -926,6 +926,36 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-9.0.1.tgz",
"integrity": "sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA=="
},
+ "@fortawesome/fontawesome-common-types": {
+ "version": "0.2.21",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.21.tgz",
+ "integrity": "sha512-iJtcrU2BtF9Wyr0zm3tHEJy3HqA6sADExhCqCv3SKaJJKKp4ORJ40t4nyHvcWXSVFtd7r1gcdqcRsAfoREGTFA=="
+ },
+ "@fortawesome/fontawesome-svg-core": {
+ "version": "1.2.21",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.21.tgz",
+ "integrity": "sha512-EhrgMZLJS0tTYZhUbodurZBqDgAFLDNdxJP/q5unrZJwiFo8Dd7xGvJdhAhY5WcX4khzkPQcbLTCMPHBtutD7Q==",
+ "requires": {
+ "@fortawesome/fontawesome-common-types": "^0.2.21"
+ }
+ },
+ "@fortawesome/free-solid-svg-icons": {
+ "version": "5.10.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.1.tgz",
+ "integrity": "sha512-MKH+SCt0DnVoXdemxf6JEdTRtCPwYLMCWZcwgGccYU/ab6QcDtbAMn6Xm4Zub6YqQCcaiy0hU294YdHOldSBRA==",
+ "requires": {
+ "@fortawesome/fontawesome-common-types": "^0.2.21"
+ }
+ },
+ "@fortawesome/react-fontawesome": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz",
+ "integrity": "sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g==",
+ "requires": {
+ "humps": "^2.0.1",
+ "prop-types": "^15.5.10"
+ }
+ },
"@hapi/address": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz",
@@ -6270,6 +6300,11 @@
}
}
},
+ "humps": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
+ "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao="
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
diff --git a/package.json b/package.json
index d03877a..48310c1 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,12 @@
{
"name": "diabloweb",
- "version": "1.0.35",
+ "version": "1.0.36",
"private": true,
"dependencies": {
"@babel/core": "7.4.3",
+ "@fortawesome/fontawesome-svg-core": "^1.2.21",
+ "@fortawesome/free-solid-svg-icons": "^5.10.1",
+ "@fortawesome/react-fontawesome": "^0.1.4",
"@svgr/webpack": "4.1.0",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.6.0",
diff --git a/src/App.js b/src/App.js
index 59f6dde..f01e1eb 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,6 +2,9 @@ import React from 'react';
import './App.scss';
import classNames from 'classnames';
import ReactGA from 'react-ga';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTimes, faDownload } from '@fortawesome/free-solid-svg-icons';
+import getPlayerName from './api/savefile';
import { mapStackTrace } from 'sourcemapped-stacktrace';
@@ -127,6 +130,7 @@ class App extends React.Component {
if (spawn && SpawnSizes.includes(spawn.byteLength)) {
this.setState({has_spawn: true});
}
+ this.updateSaves();
});
}
@@ -218,10 +222,29 @@ class App extends React.Component {
setCurrentSave(name) {
this.saveName = name;
}
- downloadSave = e => {
- this.fs.then(fs => this.saveName && fs.download(this.saveName));
- e.stopPropagation();
- e.preventDefault();
+
+ updateSaves() {
+ this.fs.then(fs => {
+ const saves = [];
+ [...fs.files.keys()].filter(name => name.match(/\.sv$/i)).forEach(name => {
+ saves.push(name);
+ console.log(name, getPlayerName(fs.files.get(name).buffer));
+ });
+ this.setState({save_names: saves});
+ });
+ }
+ removeSave(name) {
+ if (window.confirm(`Are you sure you want to delete ${name}?`)) {
+ (async () => {
+ const fs = await this.fs;
+ await fs.delete(name.toLowerCase());
+ fs.files.delete(name.toLowerCase());
+ this.updateSaves();
+ })();
+ }
+ }
+ downloadSave(name) {
+ this.fs.then(fs => fs.download(name));
}
drawBelt(idx, slot) {
@@ -260,7 +283,12 @@ class App extends React.Component {
start(file) {
if (file && file.name.match(/\.sv$/i)) {
- this.fs.then(fs => fs.upload(file)).then(console.log(`Updated ${file.name}`));
+ this.fs.then(fs => fs.upload(file)).then(() => {
+ this.updateSaves();
+ });
+ return;
+ }
+ if (this.state.show_saves) {
return;
}
if (file && !file.name.match(/\.mpq$/i)) {
@@ -455,6 +483,13 @@ class App extends React.Component {
}
}
+ parseSave = e => {
+ const files = e.target.files;
+ if (files.length > 0) {
+ this.start(files[0]);
+ }
+ }
+
touchButton = null;
touchCanvas = null;
@@ -622,7 +657,29 @@ class App extends React.Component {
}
render() {
- const {started, loading, error, progress, dropping, has_spawn} = this.state;
+ const {started, loading, error, progress, dropping, has_spawn, save_names, show_saves} = this.state;
+ if (show_saves && save_names) {
+ return (
+
+
+
+
+ {save_names.map(name => -
+ {name}
+ this.downloadSave(name)}/>
+ this.removeSave(name)}/>
+
)}
+
+
+
this.setState({show_saves: false})}>Back
+
+
+
+ );
+ }
return (
diff --git a/src/App.scss b/src/App.scss
index 94bd92a..af3e5ff 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -122,6 +122,28 @@ body, #root, .App {
background-color: #111;
}
}
+ .saveList {
+ border: 1px solid #fff;
+ text-align: left;
+ li {
+ padding: 0 6px;
+ .btnRemove {
+ color: #f88;
+ float: right;
+ cursor: pointer;
+ margin: 0 4px;
+ }
+ .btnDownload {
+ color: #fff;
+ float: right;
+ cursor: pointer;
+ margin: 0 4px;
+ }
+ &:hover {
+ background-color: #444;
+ }
+ }
+ }
}
}
diff --git a/src/api/codec.js b/src/api/codec.js
new file mode 100644
index 0000000..2bf2aa6
--- /dev/null
+++ b/src/api/codec.js
@@ -0,0 +1,164 @@
+const W = new Uint32Array(80);
+
+const SHA1CircularShift = (shift, value) => ((value << shift) | (value >>> (32 - shift)));
+
+class SHA1 {
+ state = new Uint32Array(5);
+ count = 0;
+
+ input(u8) {
+ const u32 = new Uint32Array(u8.buffer, u8.byteOffset, 16);
+ context.count += data.length * 32;
+ for (let i = 0; i < 16; ++i) {
+ W[i] = u32[i];
+ }
+ for (let i = 16; i < 80; ++i) {
+ W[i] = W[i - 16] ^ W[i - 14] ^ W[i - 8] ^ W[i - 3];
+ }
+ let A = this.state[0];
+ let B = this.state[1];
+ let C = this.state[2];
+ let D = this.state[3];
+ let E = this.state[4];
+
+ for (let i = 0; i < 20; i++) {
+ const temp = SHA1CircularShift(5, A) + ((B & C) | ((~B) & D)) + E + W[i] + 0x5A827999;
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30, B);
+ B = A;
+ A = temp | 0;
+ }
+
+ for (let i = 20; i < 40; i++) {
+ const temp = SHA1CircularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1;
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30, B);
+ B = A;
+ A = temp | 0;
+ }
+
+ for (let i = 40; i < 60; i++) {
+ const temp = SHA1CircularShift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC;
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30, B);
+ B = A;
+ A = temp | 0;
+ }
+
+ for (let i = 60; i < 80; i++) {
+ const temp = SHA1CircularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6;
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30, B);
+ B = A;
+ A = temp | 0;
+ }
+
+ this.state[0] += A;
+ this.state[1] += B;
+ this.state[2] += C;
+ this.state[3] += D;
+ this.state[4] += E;
+ }
+
+ constructor() {
+ this.state[0] = 0x67452301;
+ this.state[1] = 0xEFCDAB89;
+ this.state[2] = 0x98BADCFE;
+ this.state[3] = 0x10325476;
+ this.state[4] = 0xC3D2E1F0;
+
+ this.result = new Uint8Array(this.state.buffer);
+ }
+}
+
+class Random {
+ constructor(seed) {
+ this.seed = seed;
+ }
+ next() {
+ this.seed = (((this.seed * 3) << 16) + ((this.seed * 67) << 8) + (this.seed * 253) + 2531011) | 0;
+ return (this.seed >> 16) & 0x7FFF;
+ }
+}
+
+struct CodecSignature {
+ DWORD checksum;
+ BYTE error;
+ BYTE last_chunk_size;
+ WORD unused;
+};
+
+function codec_init_key(password) {
+ const rand = new Random(0x7058);
+ const key = new Uint8Array(136);
+ for (let i = 0; i < 136; ++i) {
+ key[i] = rand.next();
+ }
+ const pw = new Uint8Array(64);
+ for (let i = 0; i < 64; ++i) {
+ pw[i] = password.charCodeAt(i % password.length);
+ }
+
+ const sha = new SHA1();
+ sha.input(pw);
+
+ for (let i = 0; i < 136; ++i) {
+ key[i] ^= sha.result[i % sha.result.length];
+ }
+
+ sha = new SHA1();
+ sha.input(key.subarray(72));
+ return sha;
+}
+
+function codec_decode(data, password) {
+ const sha = codec_init_key(password);
+ if (data.length <= 8) {
+ return;
+ }
+ const size = data.length - 8;
+ if ()
+ char buf[128];
+ char dst[SHA1HashSize];
+ int i;
+ CodecSignature *sig;
+
+ codec_init_key(0, pszPassword);
+ if (size <= 8)
+ return 0;
+ size = size - 8;
+ if (size % 64 != 0)
+ return 0;
+ for (i = size; i != 0; pbSrcDst += 64, i -= 64) {
+ memcpy(buf, pbSrcDst, 64);
+ SHA1Result(0, dst);
+ for (int j = 0; j < 64; j++) {
+ buf[j] ^= dst[j % SHA1HashSize];
+ }
+ SHA1Calculate(0, buf, NULL);
+ memset(dst, 0, sizeof(dst));
+ memcpy(pbSrcDst, buf, 64);
+ }
+
+ memset(buf, 0, sizeof(buf));
+ sig = (CodecSignature *)pbSrcDst;
+ if (sig->error > 0) {
+ size = 0;
+ SHA1Clear();
+ } else {
+ SHA1Result(0, dst);
+ if (sig->checksum != *(DWORD *)dst) {
+ memset(dst, 0, sizeof(dst));
+ size = 0;
+ SHA1Clear();
+ } else {
+ size += sig->last_chunk_size - 64;
+ SHA1Clear();
+ }
+ }
+ return size;
+}
diff --git a/src/api/explode.js b/src/api/explode.js
new file mode 100644
index 0000000..16e2480
--- /dev/null
+++ b/src/api/explode.js
@@ -0,0 +1,443 @@
+export const CMP_BINARY = 0; // Binary compression
+export const CMP_ASCII = 1; // Ascii compression
+export const CMP_NO_ERROR = 0;
+export const CMP_INVALID_DICTSIZE = 1;
+export const CMP_INVALID_MODE = 2;
+export const CMP_BAD_DATA = 3;
+export const CMP_ABORT = 4;
+export const CMP_IMPLODE_DICT_SIZE1 = 1024; // Dictionary size of 1024
+export const CMP_IMPLODE_DICT_SIZE2 = 2048; // Dictionary size of 2048
+export const CMP_IMPLODE_DICT_SIZE3 = 4096; // Dictionary size of 4096
+
+export const PKDCL_OK = 0;
+export const PKDCL_STREAM_END = 1; // All data from the input stream is read
+export const PKDCL_NEED_DICT = 2; // Need more data (dictionary)
+export const PKDCL_CONTINUE = 10; // Internal flag, not returned to user
+export const PKDCL_GET_INPUT = 11; // Internal flag, not returned to user
+
+const DistBits = new Uint8Array([
+ 0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08
+]);
+
+const DistCode = new Uint8Array([
+ 0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A,
+ 0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C,
+ 0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08,
+ 0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00
+]);
+
+const ExLenBits = new Uint8Array([
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
+]);
+
+const LenBase = new Uint16Array([
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x000A, 0x000E, 0x0016, 0x0026, 0x0046, 0x0086, 0x0106
+]);
+
+const LenBits = new Uint8Array([
+ 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07
+]);
+
+const LenCode = new Uint8Array([
+ 0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14, 0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00
+]);
+
+const ChBitsAsc = new Uint8Array([
+ 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C,
+ 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
+ 0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08,
+ 0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B,
+ 0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06,
+ 0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x09, 0x0C, 0x08,
+ 0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05,
+ 0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C,
+ 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
+ 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
+ 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
+ 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
+ 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
+ 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
+ 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D,
+ 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D
+]);
+
+const ChCodeAsc = new Uint16Array([
+ 0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0,
+ 0x01E0, 0x00B8, 0x0062, 0x0EE0, 0x06E0, 0x0022, 0x0AE0, 0x02E0,
+ 0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360,
+ 0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60,
+ 0x000F, 0x0250, 0x0038, 0x0260, 0x0050, 0x0C60, 0x0390, 0x00D8,
+ 0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098,
+ 0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C,
+ 0x0074, 0x00E8, 0x0068, 0x0460, 0x0090, 0x0034, 0x00B0, 0x0710,
+ 0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8,
+ 0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E,
+ 0x0024, 0x0510, 0x000E, 0x0036, 0x0016, 0x0044, 0x0030, 0x00C8,
+ 0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088,
+ 0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A,
+ 0x000A, 0x000B, 0x0210, 0x0004, 0x0013, 0x0032, 0x0003, 0x001D,
+ 0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078,
+ 0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0,
+ 0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040,
+ 0x0040, 0x1F80, 0x0F80, 0x1780, 0x0780, 0x1B80, 0x0B80, 0x1380,
+ 0x0380, 0x1D80, 0x0D80, 0x1580, 0x0580, 0x1980, 0x0980, 0x1180,
+ 0x0180, 0x1E80, 0x0E80, 0x1680, 0x0680, 0x1A80, 0x0A80, 0x1280,
+ 0x0280, 0x1C80, 0x0C80, 0x1480, 0x0480, 0x1880, 0x0880, 0x1080,
+ 0x0080, 0x1F00, 0x0F00, 0x1700, 0x0700, 0x1B00, 0x0B00, 0x1300,
+ 0x0DA0, 0x05A0, 0x09A0, 0x01A0, 0x0EA0, 0x06A0, 0x0AA0, 0x02A0,
+ 0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320,
+ 0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220,
+ 0x0C20, 0x0420, 0x0820, 0x0020, 0x0FC0, 0x07C0, 0x0BC0, 0x03C0,
+ 0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0,
+ 0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340,
+ 0x0300, 0x0D40, 0x1D00, 0x0D00, 0x1500, 0x0540, 0x0500, 0x1900,
+ 0x0900, 0x0940, 0x1100, 0x0100, 0x1E00, 0x0E00, 0x0140, 0x1600,
+ 0x0600, 0x1A00, 0x0E40, 0x0640, 0x0A40, 0x0A00, 0x1200, 0x0200,
+ 0x1C00, 0x0C00, 0x1400, 0x0400, 0x1800, 0x0800, 0x1000, 0x0000
+]);
+
+/**
+ * @param {Uint8Array} positions [out] Table of positions
+ * @param {Uint8Array} start_indexes Table of start indexes
+ * @param {Uint8Array} length_bits Table of lengths. Each length is stored as number of bits
+ * @param {number} elements Number of elements in start_indexes and length_bits
+ */
+function GenDecodeTabs(positions, start_indexes, length_bits, elements) {
+ for (let i = 0; i < elements; i++) {
+ const length = 1 << length_bits[i]; // Get the length in bytes
+ for (let index = start_indexes[i]; index < 0x100; index += length) {
+ positions[index] = i;
+ }
+ }
+}
+
+function GenAscTabs(pWork) {
+ let pChCodeAsc = 0xFF;
+
+ for (let count = 0x00FF; pChCodeAsc >= 0; pChCodeAsc--, count--) {
+ let bits_asc = pWork.ChBitsAsc[count];
+ let acc;
+
+ if (bits_asc <= 8) {
+ const add = (1 << bits_asc);
+ acc = ChCodeAsc[pChCodeAsc];
+
+ do {
+ pWork.offs2C34[acc] = count;
+ acc += add;
+ } while(acc < 0x100);
+ } else if ((acc = (ChCodeAsc[pChCodeAsc] & 0xFF)) !== 0) {
+ pWork.offs2C34[acc] = 0xFF;
+
+ if (ChCodeAsc[pChCodeAsc] & 0x3F) {
+ bits_asc -= 4;
+ pWork.ChBitsAsc[count] = bits_asc;
+
+ const add = (1 << bits_asc);
+ acc = ChCodeAsc[pChCodeAsc] >> 4;
+ do {
+ pWork.offs2D34[acc] = count;
+ acc += add;
+ } while(acc < 0x100);
+ } else {
+ bits_asc -= 6;
+ pWork.ChBitsAsc[count] = bits_asc;
+
+ const add = (1 << bits_asc);
+ acc = ChCodeAsc[pChCodeAsc] >> 6;
+ do {
+ pWork.offs2E34[acc] = count;
+ acc += add;
+ } while(acc < 0x80);
+ }
+ } else {
+ bits_asc -= 8;
+ pWork.ChBitsAsc[count] = bits_asc;
+
+ const add = (1 << bits_asc);
+ acc = ChCodeAsc[pChCodeAsc] >> 8;
+ do {
+ pWork.offs2EB4[acc] = count;
+ acc += add;
+ } while(acc < 0x100);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Removes given number of bits in the bit buffer. New bits are reloaded from
+// the input buffer, if needed.
+// Returns: PKDCL_OK: Operation was successful
+// PKDCL_STREAM_END: There are no more bits in the input buffer
+function WasteBits(pWork, nBits) {
+ // If number of bits required is less than number of (bits in the buffer) ?
+ if (nBits <= pWork.extra_bits) {
+ pWork.extra_bits -= nBits;
+ pWork.bit_buff >>>= nBits;
+ return PKDCL_OK;
+ }
+
+ // Load input buffer if necessary
+ pWork.bit_buff >>= pWork.extra_bits;
+ if (pWork.in_pos === pWork.in_bytes) {
+ if ((pWork.in_bytes = pWork.read_buf(pWork.in_buff)) === 0) {
+ return PKDCL_STREAM_END;
+ }
+ pWork.in_pos = 0;
+ }
+
+ // Update bit buffer
+ pWork.bit_buff |= (pWork.in_buff[pWork.in_pos++] << 8);
+ pWork.bit_buff >>>= (nBits - pWork.extra_bits);
+ pWork.extra_bits = (pWork.extra_bits - nBits) + 8;
+ return PKDCL_OK;
+}
+
+//-----------------------------------------------------------------------------
+// Decodes next literal from the input (compressed) data.
+// Returns : 0x000: One byte 0x00
+// 0x001: One byte 0x01
+// ...
+// 0x0FF: One byte 0xFF
+// 0x100: Repetition, length of 0x02 bytes
+// 0x101: Repetition, length of 0x03 bytes
+// ...
+// 0x304: Repetition, length of 0x206 bytes
+// 0x305: End of stream
+// 0x306: Error
+function DecodeLit(pWork) {
+ if(pWork.bit_buff & 1) {
+ // Remove one bit from the input data
+ if(WasteBits(pWork, 1)) {
+ return 0x306;
+ }
+
+ // The next 8 bits hold the index to the length code table
+ let length_code = pWork.LengthCodes[pWork.bit_buff & 0xFF];
+
+ // Remove the apropriate number of bits
+ if(WasteBits(pWork, pWork.LenBits[length_code])) {
+ return 0x306;
+ }
+
+ // Are there some extra bits for the obtained length code ?
+ const extra_length_bits = pWork.ExLenBits[length_code];
+ if(extra_length_bits !== 0) {
+ const extra_length = pWork.bit_buff & ((1 << extra_length_bits) - 1);
+ if(WasteBits(pWork, extra_length_bits)) {
+ if((length_code + extra_length) != 0x10E) {
+ return 0x306;
+ }
+ }
+ length_code = pWork.LenBase[length_code] + extra_length;
+ }
+
+ // In order to distinguish uncompressed byte from repetition length,
+ // we have to add 0x100 to the length.
+ return length_code + 0x100;
+ }
+
+ // Remove one bit from the input data
+ if(WasteBits(pWork, 1)) {
+ return 0x306;
+ }
+
+ // If the binary compression type, read 8 bits and return them as one byte.
+ if(pWork.ctype === CMP_BINARY) {
+ const uncompressed_byte = pWork.bit_buff & 0xFF;
+
+ if(WasteBits(pWork, 8)) {
+ return 0x306;
+ }
+ return uncompressed_byte;
+ }
+
+ // When ASCII compression ...
+ let value;
+ if (pWork.bit_buff & 0xFF) {
+ value = pWork.offs2C34[pWork.bit_buff & 0xFF];
+
+ if (value == 0xFF) {
+ if (pWork.bit_buff & 0x3F) {
+ if (WasteBits(pWork, 4)) {
+ return 0x306;
+ }
+
+ value = pWork.offs2D34[pWork.bit_buff & 0xFF];
+ } else {
+ if (WasteBits(pWork, 6)) {
+ return 0x306;
+ }
+
+ value = pWork.offs2E34[pWork.bit_buff & 0x7F];
+ }
+ }
+ } else {
+ if(WasteBits(pWork, 8)) {
+ return 0x306;
+ }
+
+ value = pWork.offs2EB4[pWork.bit_buff & 0xFF];
+ }
+
+ return WasteBits(pWork, pWork.ChBitsAsc[value]) ? 0x306 : value;
+}
+
+//-----------------------------------------------------------------------------
+// Decodes the distance of the repetition, backwards relative to the
+// current output buffer position
+function DecodeDist(pWork, rep_length) {
+ // Next 2-8 bits in the input buffer is the distance position code
+ const dist_pos_code = pWork.DistPosCodes[pWork.bit_buff & 0xFF];
+ const dist_pos_bits = pWork.DistBits[dist_pos_code];
+ if (WasteBits(pWork, dist_pos_bits)) {
+ return 0;
+ }
+
+ let distance;
+ if (rep_length === 2) {
+ // If the repetition is only 2 bytes length,
+ // then take 2 bits from the stream in order to get the distance
+ distance = (dist_pos_code << 2) | (pWork.bit_buff & 0x03);
+ if (WasteBits(pWork, 2)) {
+ return 0;
+ }
+ } else {
+ // If the repetition is more than 2 bytes length,
+ // then take "dsize_bits" bits in order to get the distance
+ distance = (dist_pos_code << pWork.dsize_bits) | (pWork.bit_buff & pWork.dsize_mask);
+ if (WasteBits(pWork, pWork.dsize_bits)) {
+ return 0;
+ }
+ }
+ return distance + 1;
+}
+
+function Expand(pWork) {
+ let outputPos = 0x1000; // Initialize output buffer position
+
+ // Decode the next literal from the input data.
+ // The returned literal can either be an uncompressed byte (next_literal < 0x100)
+ // or an encoded length of the repeating byte sequence that
+ // is to be copied to the current buffer position
+ let result, next_literal;
+ while ((result = next_literal = DecodeLit(pWork)) < 0x305) {
+ // If the literal is greater than 0x100, it holds length
+ // of repeating byte sequence
+ // literal of 0x100 means repeating sequence of 0x2 bytes
+ // literal of 0x101 means repeating sequence of 0x3 bytes
+ // ...
+ // literal of 0x305 means repeating sequence of 0x207 bytes
+ if(next_literal >= 0x100) {
+ // Get the length of the repeating sequence.
+ // Note that the repeating block may overlap the current output position,
+ // for example if there was a sequence of equal bytes
+ let rep_length = next_literal - 0xFE;
+ // Get backward distance to the repetition
+ const minus_dist = DecodeDist(pWork, rep_length);
+ if (minus_dist === 0) {
+ result = 0x306;
+ break;
+ }
+
+ // Target and source pointer
+ let target = outputPos;
+ let source = target - minus_dist;
+
+ // Update buffer output position
+ outputPos += rep_length;
+
+ // Copy the repeating sequence
+ const out_buff = pWork.out_buff;
+ while (rep_length-- > 0) {
+ out_buff[target++] = out_buff[source++];
+ }
+ } else {
+ pWork.out_buff[outputPos++] = next_literal;
+ }
+
+ // Flush the output buffer, if number of extracted bytes has reached the end
+ if (outputPos >= 0x2000) {
+ // Copy decompressed data into user buffer
+ pWork.write_buf(pWork.out_buff.subarray(0x1000, 0x2000));
+
+ // Now copy the decompressed data to the first half of the buffer.
+ // This is needed because the decompression might reuse them as repetitions.
+ // Note that if the output buffer overflowed previously, the extra decompressed bytes
+ // are stored in "out_buff_overflow", and they will now be
+ // within decompressed part of the output buffer.
+ pWork.out_buff.copyWithin(0, 0x1000, outputPos);
+ outputPos -= 0x1000;
+ }
+ }
+
+ // Flush any remaining decompressed bytes
+ pWork.write_buf(pWork.out_buff.subarray(0x1000, outputPos));
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+// Main exploding function.
+export function explode(read_buf, write_buf) {
+ const buffer = new ArrayBuffer(0x3104);
+ const pWork = {
+ read_buf,
+ write_buf,
+ in_pos: 3,
+ extra_bits: 0,
+ in_buff: new Uint8Array(buffer, 0, 0x800),
+ DistPosCodes: new Uint8Array(buffer, 0x800, 0x100),
+ LengthCodes: new Uint8Array(buffer, 0x900, 0x100),
+ offs2C34: new Uint8Array(buffer, 0xA00, 0x100),
+ offs2D34: new Uint8Array(buffer, 0xB00, 0x100),
+ offs2E34: new Uint8Array(buffer, 0xC00, 0x80),
+ offs2EB4: new Uint8Array(buffer, 0xC80, 0x100),
+ ChBitsAsc: new Uint8Array(buffer, 0xD80, 0x100),
+ DistBits: new Uint8Array(buffer, 0xE80, 0x40),
+ LenBits: new Uint8Array(buffer, 0xEC0, 0x10),
+ ExLenBits: new Uint8Array(buffer, 0xED0, 0x10),
+ LenBase: new Uint16Array(buffer, 0xEE0, 0x10),
+ out_buff: new Uint8Array(buffer, 0xF00, 0x2204),
+ };
+ pWork.in_bytes = read_buf(pWork.in_buff);
+ if (pWork.in_bytes <= 4) {
+ return CMP_BAD_DATA;
+ }
+ pWork.ctype = pWork.in_buff[0];
+ pWork.dsize_bits = pWork.in_buff[1];
+ pWork.bit_buff = pWork.in_buff[2];
+
+ // Test for the valid dictionary size
+ if(4 > pWork.dsize_bits || pWork.dsize_bits > 6) {
+ return CMP_INVALID_DICTSIZE;
+ }
+
+ pWork.dsize_mask = 0xFFFF >> (0x10 - pWork.dsize_bits); // Shifted by 'sar' instruction
+
+ if(pWork.ctype != CMP_BINARY) {
+ if(pWork.ctype != CMP_ASCII) {
+ return CMP_INVALID_MODE;
+ }
+
+ pWork.ChBitsAsc.set(ChBitsAsc);
+ GenAscTabs(pWork);
+ }
+
+ pWork.LenBits.set(LenBits);
+ GenDecodeTabs(pWork.LengthCodes, LenCode, pWork.LenBits, pWork.LenBits.length);
+ pWork.ExLenBits.set(ExLenBits);
+ pWork.LenBase.set(LenBase);
+ pWork.DistBits.set(DistBits);
+ GenDecodeTabs(pWork.DistPosCodes, DistCode, pWork.DistBits, pWork.DistBits.length);
+ if(Expand(pWork) !== 0x306) {
+ return CMP_NO_ERROR;
+ }
+
+ return CMP_ABORT;
+}
+
+export default explode;
diff --git a/src/api/savefile.js b/src/api/savefile.js
new file mode 100644
index 0000000..728b966
--- /dev/null
+++ b/src/api/savefile.js
@@ -0,0 +1,229 @@
+import { explode } from './explode';
+
+function pkzip_decompress(data, out_size) {
+ if (data.length === out_size) {
+ return data;
+ }
+
+ const output = new Uint8Array(out_size);
+ let in_pos = 0;
+ let out_pos = 0;
+ function read_buf(dst) {
+ const count = Math.min(data.length - in_pos, dst.length);
+ dst.set(data.subarray(in_pos, count));
+ in_pos += count;
+ return count;
+ }
+ function write_buf(src) {
+ if (out_pos + src.length > out_size) {
+ throw Error('decompress buffer overflow');
+ }
+ output.set(src, out_pos);
+ out_pos += src.length;
+ }
+
+ if (explode(read_buf, write_buf) || out_pos !== out_size) {
+ return null;
+ }
+
+ return output;
+}
+
+const hashtable = (function() {
+ const hashtable = new Uint32Array(1280);
+ let seed = 0x00100001;
+ for (let i = 0; i < 256; i++) {
+ for (let j = i; j < 1280; j += 256) {
+ seed = (seed * 125 + 3) % 0x2AAAAB;
+ const a = (seed & 0xFFFF) << 16;
+ seed = (seed * 125 + 3) % 0x2AAAAB;
+ const b = (seed & 0xFFFF);
+ hashtable[j] = a | b;
+ }
+ }
+ return hashtable;
+})();
+function decrypt(u32, key) {
+ let seed = 0xEEEEEEEE;
+ for (let i = 0; i < u32.length; ++i) {
+ seed += hashtable[0x400 + (key & 0xFF)];
+ u32[i] ^= seed + key;
+ seed = (u32[i] + seed * 33 + 3) | 0;
+ key = ((~key << 0x15) + 0x11111111) | (key >>> 0x0B);
+ }
+}
+function decrypt8(u8, key) {
+ decrypt(new Uint32Array(u8.buffer, u8.byteOffset, u8.length >> 2), key);
+}
+function hash(name, type) {
+ let seed1 = 0x7FED7FED;
+ let seed2 = 0xEEEEEEEE;
+ for (let i = 0; i < name.length; ++i) {
+ let ch = name.charCodeAt(i);
+ if (ch >= 0x61 && ch <= 0x7A) {
+ ch -= 0x20;
+ }
+ if (ch === 0x2F) {
+ ch = 0x5C;
+ }
+ seed1 = hashtable[type * 256 + ch] ^ (seed1 + seed2);
+ seed2 = (ch + seed1 + seed2 * 33 + 3) | 0;
+ }
+ return seed1 >>> 0;
+}
+
+function path_name(name) {
+ const pos = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
+ return name.substring(pos + 1);
+}
+
+const Flags = {
+ CompressPkWare: 0x00000100,
+ CompressMulti: 0x00000200,
+ Compressed: 0x0000FF00,
+ Encrypted: 0x00010000,
+ FixSeed: 0x00020000,
+ PatchFile: 0x00100000,
+ SingleUnit: 0x01000000,
+ DummyFile: 0x02000000,
+ SectorCrc: 0x04000000,
+ Exists: 0x80000000,
+};
+
+class MpqReader {
+ constructor(buffer) {
+ this.buffer = buffer;
+ this.u8 = new Uint8Array(buffer);
+ this.u32 = new Uint32Array(buffer);
+
+ this.readHeader();
+ }
+
+ readHeader() {
+ const {u8, u32} = this;
+ if (u32[0] !== 0x1A51504D) {
+ throw Error('invalid MPQ header');
+ }
+ const sizeId = u8[14] + (u8[15] << 8);
+ const hashOffset = u32[4];
+ const blockOffset = u32[5];
+ const hashCount = u32[6];
+ const blockCount = u32[7];
+ this.hashTable = this.readTable(hashOffset, hashCount, "(hash table)");
+ this.blockTable = this.readTable(blockOffset, blockCount, "(block table)");
+ this.blockSize = 1 << (9 + sizeId);
+ }
+
+ readTable(offset, count, key) {
+ const buffer = new Uint32Array(this.buffer.slice(offset, offset + count * 16));
+ decrypt(buffer, hash(key, 3));
+ return buffer;
+ }
+
+ fileIndex(name) {
+ const {hashTable} = this;
+ const length = hashTable.length >> 2;
+ const index = hash(name, 0) % length;
+ const keyA = hash(name, 1), keyB = hash(name, 2);
+ for (let i = index, count = 0; hashTable[i * 4 + 3] !== 0xFFFFFFFF && count < length; i = (i + 1) % length, ++count) {
+ if (hashTable[i * 4] === keyA && hashTable[i * 4 + 1] === keyB && hashTable[i * 4 + 3] !== 0xFFFFFFFE) {
+ return i;
+ }
+ }
+ }
+
+ read(name) {
+ const index = this.fileIndex(name);
+ if (index == null) {
+ return;
+ }
+ const block = this.hashTable[index * 4 + 3];
+ const filePos = this.blockTable[block * 4];
+ let cmpSize = this.blockTable[block * 4 + 1];
+ const fileSize = this.blockTable[block * 4 + 2];
+ const flags = this.blockTable[block * 4 + 3];
+
+ if (flags & Flags.PatchFile) {
+ return;
+ }
+ if (!(flags & Flags.Compressed)) {
+ cmpSize = fileSize;
+ }
+
+ let key = hash(path_name(name), 3);
+ if (flags & Flags.FixSeed) {
+ key = (key + filePos) ^ fileSize;
+ }
+
+ if (flags & Flags.SingleUnit) {
+ const raw = new Uint8Array(this.buffer, filePos, cmpSize);
+ if (raw.length !== cmpSize) {
+ return;
+ }
+ if (flags & Flags.Encrypted) {
+ decrypt8(raw, key);
+ }
+ if (flags & Flags.CompressMulti) {
+ return;
+ } else if (flags & Flags.CompressPkWare) {
+ return pkzip_decompress(raw, fileSize);
+ }
+ return raw;
+ } else if (!(flags & Flags.Compressed)) {
+ const raw = Uint8Array(this.buffer, filePos, fileSize);
+ if (raw.length !== fileSize) {
+ return;
+ }
+ if (flags & Flags.Encrypted) {
+ for (let i = 0; i < fileSize; i += this.blockSize) {
+ decrypt8(raw.subarray(i, Math.min(fileSize, i + this.blockSize)), key + i / this.blockSize);
+ }
+ }
+ return raw;
+ } else {
+ const numBlocks = Math.floor((fileSize + this.blockSize - 1) / this.blockSize);
+ const tableSize = numBlocks + 1 + ((flags & Flags.SectorCrc) ? 1 : 0);
+ const blocks = new Uint32Array(this.buffer, filePos, tableSize);
+ if (blocks.length !== tableSize) {
+ return;
+ }
+ if (flags & Flags.Encrypted) {
+ decrypt(blocks, key - 1);
+ }
+ const output = new Uint8Array(fileSize);
+ for (let i = 0; i < numBlocks; ++i) {
+ const oPos = i * this.blockSize;
+ const cSize = blocks[i + 1] - blocks[i];
+ const uSize = Math.min(this.blockSize, fileSize - oPos);
+ let tmp = new Uint8Array(this.buffer, filePos + blocks[i], cSize);
+ if (tmp.length !== cSize) {
+ return;
+ }
+ if (flags & Flags.Encrypted) {
+ decrypt8(tmp, key + i);
+ }
+ if (flags & Flags.CompressMulti) {
+ return;
+ } else if (flags & Flags.CompressPkWare) {
+ tmp = pkzip_decompress(tmp, uSize);
+ }
+ if (!tmp || tmp.length !== uSize) {
+ return;
+ }
+ output.set(tmp, oPos);
+ }
+ return output;
+ }
+ }
+}
+
+export default function getPlayerName(data) {
+ debugger;
+ try {
+ const reader = new MpqReader(data);
+ const hero = reader.read("hero");
+ return '';
+ } catch (e) {
+ return null;
+ }
+}