mirror of
https://github.com/d07RiV/diabloweb.git
synced 2026-06-03 21:41:38 +00:00
mpq compress
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "diabloweb",
|
||||
"version": "1.0.36",
|
||||
"version": "1.0.37",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "diabloweb",
|
||||
"version": "1.0.36",
|
||||
"version": "1.0.37",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.4.3",
|
||||
|
||||
155
src/App.js
155
src/App.js
@@ -11,6 +11,7 @@ import { mapStackTrace } from 'sourcemapped-stacktrace';
|
||||
import create_fs from './fs';
|
||||
import load_game from './api/loader';
|
||||
import { SpawnSizes } from './api/load_spawn';
|
||||
import CompressMpq from './mpqcmp';
|
||||
|
||||
import Peer from 'peerjs';
|
||||
|
||||
@@ -130,7 +131,9 @@ class App extends React.Component {
|
||||
if (spawn && SpawnSizes.includes(spawn.byteLength)) {
|
||||
this.setState({has_spawn: true});
|
||||
}
|
||||
this.updateSaves();
|
||||
if ([...fs.files.keys()].filter(name => name.match(/\.sv$/i)).length) {
|
||||
this.setState({save_names: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +141,11 @@ class App extends React.Component {
|
||||
const file = getDropFile(e);
|
||||
if (file) {
|
||||
e.preventDefault();
|
||||
this.start(file);
|
||||
if (this.compressMpq) {
|
||||
this.compressMpq.start(file);
|
||||
} else {
|
||||
this.start(file);
|
||||
}
|
||||
}
|
||||
this.setState({dropping: 0});
|
||||
}
|
||||
@@ -223,12 +230,18 @@ class App extends React.Component {
|
||||
this.saveName = name;
|
||||
}
|
||||
|
||||
showSaves = () => {
|
||||
if (this.state.save_names === true) {
|
||||
this.updateSaves().then(() => this.setState({show_saves: !this.state.show_saves}));
|
||||
} else {
|
||||
this.setState({show_saves: !this.state.show_saves});
|
||||
}
|
||||
}
|
||||
updateSaves() {
|
||||
this.fs.then(fs => {
|
||||
const saves = [];
|
||||
return 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));
|
||||
saves[name] = getPlayerName(fs.files.get(name).buffer, name);
|
||||
});
|
||||
this.setState({save_names: saves});
|
||||
});
|
||||
@@ -302,7 +315,7 @@ class App extends React.Component {
|
||||
document.removeEventListener("dragleave", this.onDragLeave, true);
|
||||
this.setState({dropping: 0});
|
||||
|
||||
const retail = !!(file && file.name.match(/^diabdat\.mpq$/i));
|
||||
const retail = !!(file && !file.name.match(/^spawn\.mpq$/i));
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
ReactGA.event({
|
||||
category: 'Game',
|
||||
@@ -656,30 +669,78 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {started, loading, error, progress, dropping, has_spawn, save_names, show_saves} = this.state;
|
||||
if (show_saves && save_names) {
|
||||
renderUi() {
|
||||
const {started, loading, error, progress, has_spawn, save_names, show_saves, compress} = this.state;
|
||||
if (show_saves && typeof save_names === "object") {
|
||||
const plrClass = ["Warrior", "Rogue", "Sorcerer"];
|
||||
return (
|
||||
<div className={classNames("App", {touch: this.touchControls, started, dropping, keyboard: !!this.showKeyboard})} ref={this.setElement}>
|
||||
<div className="BodyV">
|
||||
<div className="start">
|
||||
<ul className="saveList">
|
||||
{save_names.map(name => <li key={name}>
|
||||
{name}
|
||||
<FontAwesomeIcon className="btnDownload" icon={faDownload} onClick={() => this.downloadSave(name)}/>
|
||||
<FontAwesomeIcon className="btnRemove" icon={faTimes} onClick={() => this.removeSave(name)}/>
|
||||
</li>)}
|
||||
</ul>
|
||||
<form>
|
||||
<label htmlFor="loadFile" className="startButton">Upload Save</label>
|
||||
<input accept=".sv" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseSave}/>
|
||||
</form>
|
||||
<span className="startButton" onClick={() => this.setState({show_saves: false})}>Back</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="start">
|
||||
<ul className="saveList">
|
||||
{Object.entries(save_names).map(([name, info]) => <li key={name}>
|
||||
{name}{info ? <span className="info">{info.name} (lv. {info.level} {plrClass[info.cls]})</span> : ""}
|
||||
<FontAwesomeIcon className="btnDownload" icon={faDownload} onClick={() => this.downloadSave(name)}/>
|
||||
<FontAwesomeIcon className="btnRemove" icon={faTimes} onClick={() => this.removeSave(name)}/>
|
||||
</li>)}
|
||||
</ul>
|
||||
<form>
|
||||
<label htmlFor="loadFile" className="startButton">Upload Save</label>
|
||||
<input accept=".sv" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseSave}/>
|
||||
</form>
|
||||
<div className="startButton" onClick={() => this.setState({show_saves: false})}>Back</div>
|
||||
</div>
|
||||
);
|
||||
} else if (compress) {
|
||||
return (
|
||||
<CompressMpq api={this} ref={e => this.compressMpq = e}/>
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<Link className="error" href={reportLink(error, this.state.retail)}>
|
||||
<p className="header">The following error has occurred:</p>
|
||||
<p className="body">{error.message}</p>
|
||||
<p className="footer">Click to create an issue on GitHub</p>
|
||||
{error.save != null && <a href={error.save} download={this.saveName}>Download save file</a>}
|
||||
</Link>
|
||||
);
|
||||
} else if (loading && !started) {
|
||||
return (
|
||||
<div className="loading">
|
||||
{(progress && progress.text) || 'Loading...'}
|
||||
{progress != null && !!progress.total && (
|
||||
<span className="progressBar"><span><span style={{width: `${Math.round(100 * progress.loaded / progress.total)}%`}}/></span></span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (!started) {
|
||||
return (
|
||||
<div className="start">
|
||||
<p>
|
||||
This is a web port of the original Diablo game, based on source code reconstructed by
|
||||
GalaXyHaXz and devilution team. The project page with information and links can be found over here <Link href="https://github.com/d07RiV/diabloweb">https://github.com/d07RiV/diabloweb</Link>
|
||||
</p>
|
||||
<p>
|
||||
If you own the original game, you can drop the original DIABDAT.MPQ onto this page or click the button below to start playing.
|
||||
The game can be purchased from <Link href="https://www.gog.com/game/diablo">GoG</Link>.
|
||||
{" "}<span className="link" onClick={() => this.setState({compress: true})}>Click here to compress the MPQ, greatly reducing its size.</span>
|
||||
</p>
|
||||
{!has_spawn && (
|
||||
<p>
|
||||
Or you can play the shareware version for free (50MB download).
|
||||
</p>
|
||||
)}
|
||||
<form>
|
||||
<label htmlFor="loadFile" className="startButton">Select MPQ</label>
|
||||
<input accept=".mpq" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseFile}/>
|
||||
</form>
|
||||
<div className="startButton" onClick={() => this.start()}>Play Shareware</div>
|
||||
{!!save_names && <div className="startButton" onClick={this.showSaves}>Manage Saves</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {started, error, dropping} = this.state;
|
||||
return (
|
||||
<div className={classNames("App", {touch: this.touchControls, started, dropping, keyboard: !!this.showKeyboard})} ref={this.setElement}>
|
||||
<div className="touch-ui touch-mods">
|
||||
@@ -699,45 +760,7 @@ class App extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="BodyV">
|
||||
{!!error && (
|
||||
<Link className="error" href={reportLink(error, this.state.retail)}>
|
||||
<p className="header">The following error has occurred:</p>
|
||||
<p className="body">{error.message}</p>
|
||||
<p className="footer">Click to create an issue on GitHub</p>
|
||||
{error.save != null && <a href={error.save} download={this.saveName}>Download save file</a>}
|
||||
</Link>
|
||||
)}
|
||||
{!!loading && !started && !error && (
|
||||
<div className="loading">
|
||||
{(progress && progress.text) || 'Loading...'}
|
||||
{progress != null && !!progress.total && (
|
||||
<span className="progressBar"><span><span style={{width: `${Math.round(100 * progress.loaded / progress.total)}%`}}/></span></span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!started && !loading && !error && (
|
||||
<div className="start">
|
||||
<p>
|
||||
This is a web port of the original Diablo game, based on source code reconstructed by
|
||||
GalaXyHaXz and devilution team. The project page with information and links can be found over here <Link href="https://github.com/d07RiV/diabloweb">https://github.com/d07RiV/diabloweb</Link>
|
||||
</p>
|
||||
<p>
|
||||
If you own the original game, you can drop the original DIABDAT.MPQ onto this page or click the button below to start playing.
|
||||
The game can be purchased from <Link href="https://www.gog.com/game/diablo">GoG</Link>.
|
||||
</p>
|
||||
{!has_spawn && (
|
||||
<p>
|
||||
Or you can play the shareware version for free (50MB download).
|
||||
</p>
|
||||
)}
|
||||
<form>
|
||||
<label htmlFor="loadFile" className="startButton">Select MPQ</label>
|
||||
<input accept=".mpq" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseFile}/>
|
||||
</form>
|
||||
<span className="startButton" onClick={() => this.start()}>Play Shareware</span>
|
||||
{!!(save_names && save_names.length) && <span className="startButton" onClick={() => this.setState({show_saves: true})}>Manage Saves</span>}
|
||||
</div>
|
||||
)}
|
||||
{this.renderUi()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
18
src/App.scss
18
src/App.scss
@@ -110,7 +110,9 @@ body, #root, .App {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.startButton {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border: 1px solid #fff;
|
||||
background: #000;
|
||||
font-size: 2em;
|
||||
@@ -127,14 +129,24 @@ body, #root, .App {
|
||||
text-align: left;
|
||||
li {
|
||||
padding: 0 6px;
|
||||
.info {
|
||||
color: #888;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.btnRemove {
|
||||
color: #f88;
|
||||
color: #800;
|
||||
&:hover {
|
||||
color: #f00;
|
||||
}
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.btnDownload {
|
||||
color: #fff;
|
||||
color: #888;
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin: 0 4px;
|
||||
|
||||
129
src/api/codec.js
129
src/api/codec.js
@@ -1,25 +1,28 @@
|
||||
const W = new Uint32Array(80);
|
||||
|
||||
const SHA1CircularShift = (shift, value) => ((value << shift) | (value >>> (32 - shift)));
|
||||
const SHA1CircularShift = (shift, value) => ((value << shift) | (value >> (32 - shift)));
|
||||
|
||||
class SHA1 {
|
||||
state = new Uint32Array(5);
|
||||
digest = new Uint32Array(5);
|
||||
count = 0;
|
||||
|
||||
input(u8) {
|
||||
input8(u8) {
|
||||
const u32 = new Uint32Array(u8.buffer, u8.byteOffset, 16);
|
||||
context.count += data.length * 32;
|
||||
this.input(u32);
|
||||
}
|
||||
input(u32) {
|
||||
this.count += u32.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];
|
||||
let A = this.digest[0];
|
||||
let B = this.digest[1];
|
||||
let C = this.digest[2];
|
||||
let D = this.digest[3];
|
||||
let E = this.digest[4];
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const temp = SHA1CircularShift(5, A) + ((B & C) | ((~B) & D)) + E + W[i] + 0x5A827999;
|
||||
@@ -57,21 +60,21 @@ class SHA1 {
|
||||
A = temp | 0;
|
||||
}
|
||||
|
||||
this.state[0] += A;
|
||||
this.state[1] += B;
|
||||
this.state[2] += C;
|
||||
this.state[3] += D;
|
||||
this.state[4] += E;
|
||||
this.digest[0] += A;
|
||||
this.digest[1] += B;
|
||||
this.digest[2] += C;
|
||||
this.digest[3] += D;
|
||||
this.digest[4] += E;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.state[0] = 0x67452301;
|
||||
this.state[1] = 0xEFCDAB89;
|
||||
this.state[2] = 0x98BADCFE;
|
||||
this.state[3] = 0x10325476;
|
||||
this.state[4] = 0xC3D2E1F0;
|
||||
this.digest[0] = 0x67452301;
|
||||
this.digest[1] = 0xEFCDAB89;
|
||||
this.digest[2] = 0x98BADCFE;
|
||||
this.digest[3] = 0x10325476;
|
||||
this.digest[4] = 0xC3D2E1F0;
|
||||
|
||||
this.result = new Uint8Array(this.state.buffer);
|
||||
this.digest8 = new Uint8Array(this.digest.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,16 +88,10 @@ class Random {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const k32 = new Uint32Array(key.buffer);
|
||||
for (let i = 0; i < 136; ++i) {
|
||||
key[i] = rand.next();
|
||||
}
|
||||
@@ -103,62 +100,50 @@ function codec_init_key(password) {
|
||||
pw[i] = password.charCodeAt(i % password.length);
|
||||
}
|
||||
|
||||
const sha = new SHA1();
|
||||
sha.input(pw);
|
||||
let sha = new SHA1();
|
||||
sha.input8(pw);
|
||||
|
||||
for (let i = 0; i < 136; ++i) {
|
||||
key[i] ^= sha.result[i % sha.result.length];
|
||||
for (let i = 0; i < 34; ++i) {
|
||||
k32[i] ^= sha.digest[i % sha.digest.length];
|
||||
}
|
||||
|
||||
sha = new SHA1();
|
||||
sha.input(key.subarray(72));
|
||||
sha.input(k32.subarray(18));
|
||||
return sha;
|
||||
}
|
||||
|
||||
function codec_decode(data, password) {
|
||||
const sha = codec_init_key(password);
|
||||
export default function codec_decode(data, password) {
|
||||
if (data.length <= 8) {
|
||||
return;
|
||||
}
|
||||
const size = data.length - 8;
|
||||
if ()
|
||||
char buf[128];
|
||||
char dst[SHA1HashSize];
|
||||
int i;
|
||||
CodecSignature *sig;
|
||||
if (size % 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (data[size + 4]) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
const last_size = data[size + 5];
|
||||
const result_size = size + last_size - 64;
|
||||
const result = new Uint8Array(result_size);
|
||||
|
||||
const sha = codec_init_key(password);
|
||||
const size32 = size >> 2;
|
||||
const data32 = new Uint32Array(data.buffer, data.byteOffset, size32 + 1);
|
||||
const buf32 = new Uint32Array(16);
|
||||
const buf = new Uint8Array(buf32.buffer);
|
||||
|
||||
for (let i = 0; i < size32; i += 16) {
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
buf32[j] = data32[i + j] ^ sha.digest[j % sha.digest.length];
|
||||
}
|
||||
sha.input(buf32);
|
||||
result.set(i === size32 - 16 ? buf.subarray(0, last_size) : buf, i * 4);
|
||||
}
|
||||
if (data32[size32] !== sha.digest[0]) {
|
||||
return;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { explode } from './explode';
|
||||
import codec_decode from './codec';
|
||||
|
||||
function pkzip_decompress(data, out_size) {
|
||||
if (data.length === out_size) {
|
||||
@@ -43,7 +44,7 @@ const hashtable = (function() {
|
||||
}
|
||||
return hashtable;
|
||||
})();
|
||||
function decrypt(u32, key) {
|
||||
export function decrypt(u32, key) {
|
||||
let seed = 0xEEEEEEEE;
|
||||
for (let i = 0; i < u32.length; ++i) {
|
||||
seed += hashtable[0x400 + (key & 0xFF)];
|
||||
@@ -52,10 +53,23 @@ function decrypt(u32, key) {
|
||||
key = ((~key << 0x15) + 0x11111111) | (key >>> 0x0B);
|
||||
}
|
||||
}
|
||||
function decrypt8(u8, key) {
|
||||
export function decrypt8(u8, key) {
|
||||
decrypt(new Uint32Array(u8.buffer, u8.byteOffset, u8.length >> 2), key);
|
||||
}
|
||||
function hash(name, type) {
|
||||
export function encrypt(u32, key) {
|
||||
let seed = 0xEEEEEEEE;
|
||||
for (let i = 0; i < u32.length; ++i) {
|
||||
seed += hashtable[0x400 + (key & 0xFF)];
|
||||
const orig = u32[i];
|
||||
u32[i] ^= seed + key;
|
||||
seed = (orig + seed * 33 + 3) | 0;
|
||||
key = ((~key << 0x15) + 0x11111111) | (key >>> 0x0B);
|
||||
}
|
||||
}
|
||||
export function encrypt8(u8, key) {
|
||||
encrypt(new Uint32Array(u8.buffer, u8.byteOffset, u8.length >> 2), key);
|
||||
}
|
||||
export function hash(name, type) {
|
||||
let seed1 = 0x7FED7FED;
|
||||
let seed2 = 0xEEEEEEEE;
|
||||
for (let i = 0; i < name.length; ++i) {
|
||||
@@ -72,7 +86,7 @@ function hash(name, type) {
|
||||
return seed1 >>> 0;
|
||||
}
|
||||
|
||||
function path_name(name) {
|
||||
export function path_name(name) {
|
||||
const pos = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
|
||||
return name.substring(pos + 1);
|
||||
}
|
||||
@@ -90,11 +104,11 @@ const Flags = {
|
||||
Exists: 0x80000000,
|
||||
};
|
||||
|
||||
class MpqReader {
|
||||
export class MpqReader {
|
||||
constructor(buffer) {
|
||||
this.buffer = buffer;
|
||||
this.u8 = new Uint8Array(buffer);
|
||||
this.u32 = new Uint32Array(buffer);
|
||||
this.u32 = new Uint32Array(buffer, 0, buffer.byteLength >> 2);
|
||||
|
||||
this.readHeader();
|
||||
}
|
||||
@@ -132,79 +146,81 @@ class MpqReader {
|
||||
}
|
||||
}
|
||||
|
||||
read(name) {
|
||||
readRaw(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) {
|
||||
const info = {
|
||||
filePos: this.blockTable[block * 4],
|
||||
cmpSize: this.blockTable[block * 4 + 1],
|
||||
fileSize: this.blockTable[block * 4 + 2],
|
||||
flags: this.blockTable[block * 4 + 3],
|
||||
key: hash(path_name(name), 3),
|
||||
};
|
||||
if ((info.flags & Flags.PatchFile) || info.filePos + info.cmpSize > this.buffer.byteLength) {
|
||||
return;
|
||||
}
|
||||
if (!(flags & Flags.Compressed)) {
|
||||
cmpSize = fileSize;
|
||||
if (!(info.flags & Flags.Compressed)) {
|
||||
info.cmpSize = info.fileSize;
|
||||
}
|
||||
|
||||
let key = hash(path_name(name), 3);
|
||||
if (flags & Flags.FixSeed) {
|
||||
key = (key + filePos) ^ fileSize;
|
||||
if (info.flags & Flags.FixSeed) {
|
||||
info.key = (info.key + info.filePos) ^ info.fileSize;
|
||||
}
|
||||
return {info, data: new Uint8Array(this.buffer, info.filePos, info.cmpSize)};
|
||||
}
|
||||
|
||||
if (flags & Flags.SingleUnit) {
|
||||
const raw = new Uint8Array(this.buffer, filePos, cmpSize);
|
||||
if (raw.length !== cmpSize) {
|
||||
read(name) {
|
||||
const raw = this.readRaw(name);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
let {info, data} = raw;
|
||||
data = data.slice();
|
||||
|
||||
if (info.flags & Flags.SingleUnit) {
|
||||
if (info.flags & Flags.Encrypted) {
|
||||
decrypt8(data, info.key);
|
||||
}
|
||||
if (info.flags & Flags.CompressMulti) {
|
||||
return;
|
||||
} else if (info.flags & Flags.CompressPkWare) {
|
||||
return pkzip_decompress(data, info.fileSize);
|
||||
}
|
||||
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 data;
|
||||
} else if (!(info.flags & Flags.Compressed)) {
|
||||
if (info.flags & Flags.Encrypted) {
|
||||
for (let i = 0; i < info.fileSize; i += this.blockSize) {
|
||||
decrypt8(data.subarray(i, Math.min(info.fileSize, i + this.blockSize)), info.key + i / this.blockSize);
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
return data;
|
||||
} 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) {
|
||||
const numBlocks = Math.floor((info.fileSize + this.blockSize - 1) / this.blockSize);
|
||||
const tableSize = numBlocks + 1;
|
||||
if (data.length < tableSize * 4) {
|
||||
return;
|
||||
}
|
||||
if (flags & Flags.Encrypted) {
|
||||
decrypt(blocks, key - 1);
|
||||
const blocks = new Uint32Array(data.buffer, 0, tableSize);
|
||||
if (info.flags & Flags.Encrypted) {
|
||||
decrypt(blocks, info.key - 1);
|
||||
}
|
||||
const output = new Uint8Array(fileSize);
|
||||
const output = new Uint8Array(info.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) {
|
||||
const uSize = Math.min(this.blockSize, info.fileSize - oPos);
|
||||
if (blocks[i + 1] > data.length) {
|
||||
return;
|
||||
}
|
||||
if (flags & Flags.Encrypted) {
|
||||
decrypt8(tmp, key + i);
|
||||
let tmp = data.subarray(blocks[i], blocks[i + 1]);
|
||||
if (info.flags & Flags.Encrypted) {
|
||||
// this is not safe, but our files are small enough
|
||||
decrypt8(tmp, info.key + i);
|
||||
}
|
||||
if (flags & Flags.CompressMulti) {
|
||||
if (info.flags & Flags.CompressMulti) {
|
||||
return;
|
||||
} else if (flags & Flags.CompressPkWare) {
|
||||
} else if (info.flags & Flags.CompressPkWare) {
|
||||
tmp = pkzip_decompress(tmp, uSize);
|
||||
}
|
||||
if (!tmp || tmp.length !== uSize) {
|
||||
@@ -217,12 +233,28 @@ class MpqReader {
|
||||
}
|
||||
}
|
||||
|
||||
export default function getPlayerName(data) {
|
||||
debugger;
|
||||
function getPassword(name) {
|
||||
if (name.match(/spawn\d+\.sv/i)) {
|
||||
return 'lshbkfg1'; // single, spawn
|
||||
} else if (name.match(/share_\d+\.sv/i)) {
|
||||
return 'lshbkfg1'; // multi, spawn
|
||||
} else if (name.match(/multi_\d+\.sv/i)) {
|
||||
return 'szqnlsk1'; // multi, retail
|
||||
} else {
|
||||
return 'xrgyrkj1'; // single, retail
|
||||
}
|
||||
}
|
||||
|
||||
export default function getPlayerName(data, name) {
|
||||
try {
|
||||
const reader = new MpqReader(data);
|
||||
const hero = reader.read("hero");
|
||||
return '';
|
||||
const hero = codec_decode(reader.read("hero"), getPassword(name));
|
||||
const nameEnd = hero.indexOf(0, 16);
|
||||
const result = {};
|
||||
result.name = String.fromCharCode(...hero.subarray(16, nameEnd));
|
||||
result.cls = hero[48];
|
||||
result.level = hero[53];
|
||||
return result;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
27
src/mpqcmp/compress.js
Normal file
27
src/mpqcmp/compress.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Worker from './mpqcmp.worker.js';
|
||||
|
||||
export default function compress(mpq, progress) {
|
||||
progress("Loading...");
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const worker = new Worker();
|
||||
worker.addEventListener("message", ({data}) => {
|
||||
switch (data.action) {
|
||||
case "result":
|
||||
resolve(data.result);
|
||||
break;
|
||||
case "error":
|
||||
reject({message: data.error, stack: data.stack});
|
||||
break;
|
||||
case "progress":
|
||||
progress(data.text, data.loaded, data.total);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
worker.postMessage({action: "run", mpq});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
84
src/mpqcmp/index.js
Normal file
84
src/mpqcmp/index.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import compress from './compress';
|
||||
|
||||
export default class CompressMpq extends React.Component {
|
||||
state = {};
|
||||
|
||||
parseFile = e => {
|
||||
const files = e.target.files;
|
||||
if (files.length > 0) {
|
||||
this.start(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(progress) {
|
||||
this.setState({progress});
|
||||
}
|
||||
onDone = result => {
|
||||
const blob = new Blob([result], {type: 'binary/octet-stream'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
this.setState({url});
|
||||
|
||||
const lnk = document.createElement('a');
|
||||
lnk.setAttribute('href', url);
|
||||
lnk.setAttribute('download', 'DIABDAT.MPQ');
|
||||
document.body.appendChild(lnk);
|
||||
lnk.click();
|
||||
document.body.removeChild(lnk);
|
||||
}
|
||||
onError(message, stack) {
|
||||
const { api } = this.props;
|
||||
api.setState({compress: false});
|
||||
api.onError(message, stack);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
if (this.state.url) {
|
||||
URL.revokeObjectURL(this.state.url);
|
||||
}
|
||||
this.props.api.setState({compress: false});
|
||||
}
|
||||
|
||||
start(file) {
|
||||
this.setState({started: true});
|
||||
compress(file, (text, loaded, total) => this.onProgress({text, loaded, total}))
|
||||
.then(this.onDone, e => this.onError(e.message, e.stack));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { url, started, progress } = this.state;
|
||||
if (url) {
|
||||
return (
|
||||
<div className="start">
|
||||
<p>
|
||||
<a href={url} download="DIABDAT.MPQ">Click here if download doesn't start.</a>
|
||||
</p>
|
||||
<div className="startButton" onClick={this.onClose}>Back</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (started) {
|
||||
return (
|
||||
<div className="loading">
|
||||
{(progress && progress.text) || 'Processing...'}
|
||||
{progress != null && !!progress.total && (
|
||||
<span className="progressBar"><span><span style={{width: `${Math.round(100 * progress.loaded / progress.total)}%`}}/></span></span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="start">
|
||||
<p>
|
||||
You can use this tool to reduce the original MPQ to about half its size. It encodes sounds in MP3 format and uses better compression for regular files.
|
||||
To begin, click the button below or drop the MPQ onto the page.
|
||||
</p>
|
||||
<form>
|
||||
<label htmlFor="loadFile" className="startButton">Select MPQ</label>
|
||||
<input accept=".mpq" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseFile}/>
|
||||
</form>
|
||||
<div className="startButton" onClick={this.onClose}>Back</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,6 @@ const MpqSize = 356747;
|
||||
/* eslint-disable-next-line no-restricted-globals */
|
||||
const worker = self;
|
||||
|
||||
function onError(err, action="error") {
|
||||
if (err instanceof Error) {
|
||||
worker.postMessage({action, error: err.toString(), stack: err.stack});
|
||||
} else {
|
||||
worker.postMessage({action, error: err.toString()});
|
||||
}
|
||||
}
|
||||
|
||||
let input_file = null;
|
||||
let output_file = null;
|
||||
let last_progress = 0;
|
||||
@@ -31,6 +23,7 @@ const DApi = {
|
||||
array.set(input_file.subarray(offset, offset + array.byteLength));
|
||||
},
|
||||
put_file_size(size) {
|
||||
debugger;
|
||||
output_file = new Uint8Array(size);
|
||||
},
|
||||
put_file_contents(array, offset) {
|
||||
@@ -65,25 +58,27 @@ const readFile = (file, progress) => new Promise((resolve, reject) => {
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
|
||||
async function initWasm(spawn, progress) {
|
||||
async function initWasm(progress) {
|
||||
const binary = await axios.request({
|
||||
url: spawn ? SpawnBinary : DiabloBinary,
|
||||
url: MpqBinary,
|
||||
responseType: 'arraybuffer',
|
||||
onDownloadProgress: progress,
|
||||
});
|
||||
const result = await (spawn ? SpawnModule : DiabloModule)({wasmBinary: binary.data}).ready;
|
||||
progress({loaded: 2000000});
|
||||
const result = await MpqModule({
|
||||
wasmBinary: binary.data,
|
||||
}).ready;
|
||||
progress({loaded: MpqSize});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function run(mpq) {
|
||||
progress("Loading...");
|
||||
let mpqLoaded = 0, mpqTotal = (mpq ? mpq.size : 0), wasmLoaded = 0, wasmTotal = (spawn ? SpawnSize : DiabloSize);
|
||||
let mpqLoaded = 0, mpqTotal = (mpq ? mpq.size : 0), wasmLoaded = 0, wasmTotal = MpqSize;
|
||||
const wasmWeight = 5;
|
||||
function updateProgress() {
|
||||
progress("Loading...", mpqLoaded + wasmLoaded * wasmWeight, mpqTotal + wasmTotal * wasmWeight);
|
||||
}
|
||||
const loadWasm = initWasm(spawn, e => {
|
||||
const loadWasm = initWasm(e => {
|
||||
wasmLoaded = Math.min(e.loaded, wasmTotal);
|
||||
updateProgress();
|
||||
});
|
||||
@@ -95,7 +90,7 @@ async function run(mpq) {
|
||||
|
||||
input_file = new Uint8Array(mpq);
|
||||
|
||||
progress("Initializing...");
|
||||
progress("Processing...");
|
||||
|
||||
wasm._DApi_MpqCmp(input_file.length);
|
||||
|
||||
@@ -105,10 +100,10 @@ async function run(mpq) {
|
||||
worker.addEventListener("message", ({data}) => {
|
||||
switch (data.action) {
|
||||
case "run":
|
||||
init_game(data.mpq).then(
|
||||
res => worker.postMessage({action: "result", data: res}, [res]),
|
||||
e => onError(e, "failed"));
|
||||
run(data.mpq).then(
|
||||
result => worker.postMessage({action: "result", result}, [result]),
|
||||
err => worker.postMessage({action: "error", error: err.toString(), stack: err.stack}));
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user