Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Atma/GoMule stash support #79

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/stash/Atma_GoMule.d2x
Binary file not shown.
170 changes: 112 additions & 58 deletions src/d2/stash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { BitWriter } from "../binary/bitwriter";
import * as items from "./items";
import { enhanceItems } from "./attribute_enhancer";
import { BitReader } from "../binary/bitreader";
import { config } from "chai";
import { getConstantData } from "./constants";

const defaultConfig = {
Expand All @@ -13,81 +12,129 @@ const defaultConfig = {
export async function read(
buffer: Uint8Array,
constants?: types.IConstantData,
version?: number | null,
userConfig?: types.IConfig
version?: number | null
): Promise<types.IStash> {
const stash = {} as types.IStash;
const stash = { pages: [] as types.IStashPage[] } as types.IStash;
const reader = new BitReader(buffer);
const config = Object.assign(defaultConfig, userConfig);
const firstHeader = reader.ReadUInt32();
stash.type = readStashHeader(reader);
reader.SeekByte(0);
if (firstHeader == 0xaa55aa55) {
stash.pages = [];
// Resurrected
if (stash.type === types.EStashType.D2R) {
let pageCount = 0;
while (reader.offset < reader.bits.length) {
pageCount++;
await readStashHeader(stash, reader);
await readStashMeta(stash, reader);
const saveVersion = version || parseInt(stash.version);
if (!constants) {
constants = getConstantData(saveVersion);
}
await readStashPart(stash, reader, saveVersion, constants);
}
stash.pageCount = pageCount;
} else {
await readStashHeader(stash, reader);
}
// PlugY/Atma/Goule
else {
await readStashMeta(stash, reader);
const saveVersion = version || parseInt(stash.version);
if (!constants) {
constants = getConstantData(saveVersion);
}
await readStashPages(stash, reader, saveVersion, constants);
// PluggY
if (stash.type === types.EStashType.SSS || stash.type === types.EStashType.CSTM) {
await readStashPages(stash, reader, saveVersion, constants);
}
// Atma/GoMule
else if (stash.type === types.EStashType.D2X) {
const stashItems = [] as types.IItem[];
reader.SeekByte(11);
for (let i = 0; i < stash.itemCount; i++) {
stashItems.push(await items.readItem(reader, saveVersion, constants, {}));
}
stash.pages.push({ items: stashItems } as types.IStashPage);
}
}
return stash;
}

async function readStashHeader(stash: types.IStash, reader: BitReader) {
const header = reader.ReadUInt32();
switch (header) {
// Resurrected
case 0xaa55aa55:
stash.type = types.EStashType.shared;
stash.hardcore = reader.ReadUInt32() == 0;
stash.version = reader.ReadUInt32().toString();
stash.sharedGold = reader.ReadUInt32();
reader.ReadUInt32(); // size of the sector
reader.SkipBytes(44); // empty
break;
// LoD
case 0x535353: // SSS
case 0x4d545343: // CSTM
stash.version = reader.ReadString(2);
if (stash.version !== "01" && stash.version !== "02") {
throw new Error(`unkown stash version ${stash.version} at position ${reader.offset - 2 * 8}`);
}

stash.type = header === 0x535353 ? types.EStashType.shared : types.EStashType.private;

if (stash.type === types.EStashType.shared && stash.version == "02") {
stash.sharedGold = reader.ReadUInt32();
}

if (stash.type === types.EStashType.private) {
reader.ReadUInt32();
stash.sharedGold = 0;
}
function readStashHeader(reader: BitReader): types.EStashType {
reader.SeekBit(0);
const header32 = reader.ReadUInt32();
// Resurrected
if (header32 === 0xaa55aa55) {
return types.EStashType.D2R;
}
// SSS (PlugY)
else if (header32 === 0x535353) {
return types.EStashType.SSS;
}
// CSTM (PlugY)
else if (header32 === 0x4d545343) {
return types.EStashType.CSTM;
}
// Check 24 bit header
reader.SeekBit(0);
const header24 = reader.ReadUInt32(24);
// D2X (Atma/GoMule)
if (header24 === 0x583244) {
return types.EStashType.D2X;
}
const header24Hex = header24?.toString(16);
const header32Hex = header32?.toString(16);
throw new Error(`unknown stash header at position 0: 24bit = 0x${header24Hex}, 32bit = 0x${header32Hex}`);
}

stash.pageCount = reader.ReadUInt32();
break;
default:
debugger;
throw new Error(
`shared stash header 'SSS' / 0xAA55AA55 / private stash header 'CSTM' not found at position ${reader.offset - 3 * 8}`
);
async function readStashMeta(stash: types.IStash, reader: BitReader) {
// Skip header, already parsed into stash type
reader.SkipBytes(4);
// Resurrected
if (stash.type === types.EStashType.D2R) {
stash.shared = true;
stash.hardcore = reader.ReadUInt32() == 0;
stash.version = reader.ReadUInt32().toString();
stash.sharedGold = reader.ReadUInt32();
reader.ReadUInt32(); // size of the sector
reader.SkipBytes(44); // empty
}
// SSS or CSTM (PlugY)
else if (stash.type === types.EStashType.SSS || stash.type === types.EStashType.CSTM) {
if (stash.type === types.EStashType.SSS) {
stash.type = types.EStashType.SSS;
stash.shared = true;
}
else if (stash.type === types.EStashType.CSTM) {
stash.type = types.EStashType.CSTM;
stash.shared = false;
}
stash.version = reader.ReadString(2);
if (stash.version !== "01" && stash.version !== "02") {
throw new Error(`unknown stash version ${stash.version} at position ${reader.offset - 16}`);
}
if (stash.shared && stash.version == "02") {
stash.sharedGold = reader.ReadUInt32();
}
if (!stash.shared) {
reader.ReadUInt32();
stash.sharedGold = 0;
}
stash.pageCount = reader.ReadUInt32();
}
// D2X (Atma/GoMule)
else if (stash.type === types.EStashType.D2X) {
stash.type = types.EStashType.D2X;
stash.shared = true;
reader.SeekByte(3);
stash.itemCount = reader.ReadUInt16();
stash.version = reader.ReadUInt16().toString();
if (stash.version !== "99") {
throw new Error(`unknown stash version ${stash.version} at position ${reader.offset - 16}`);
}
}
else {
throw new Error(`unknown stash type: ${stash.type}`);
}
}

async function readStashPages(stash: types.IStash, reader: BitReader, version: number, constants: types.IConstantData) {
stash.pages = [];
for (let i = 0; i < stash.pageCount; i++) {
await readStashPage(stash, reader, version, constants);
}
Expand Down Expand Up @@ -134,33 +181,40 @@ export async function write(
if (!constants) {
constants = getConstantData(version);
}
if (version > 0x61) {
// Resurrected
if (data.type === types.EStashType.D2R) {
for (const page of data.pages) {
writer.WriteArray(await writeStashSection(data, page, constants, config));
}
} else {
}
// SSS or CSTM (PlugY)
else if (data.type === types.EStashType.SSS || data.type === types.EStashType.CSTM) {
writer.WriteArray(await writeStashHeader(data));
writer.WriteArray(await writeStashPages(data, version, constants, config));
}
// D2X (Atma/GoMule)
else if (data.type === types.EStashType.D2X) {
throw new Error('No write support for D2X (Atma/GoMule)');
}
return writer.ToArray();
}

async function writeStashHeader(data: types.IStash): Promise<Uint8Array> {
const writer = new BitWriter();
if (data.type === types.EStashType.private) {
writer.WriteString("CSTM", 4);
} else {
if (data.shared) {
writer.WriteString("SSS", 4);
} else {
writer.WriteString("CSTM", 4);
}

writer.WriteString(data.version, data.version.length);

if (data.type === types.EStashType.private) {
writer.WriteString("", 4);
} else {
if (data.shared) {
if (data.version == "02") {
writer.WriteUInt32(data.sharedGold);
}
} else {
writer.WriteString("", 4);
}
writer.WriteUInt32(data.pages.length);
return writer.ToArray();
Expand Down
8 changes: 6 additions & 2 deletions src/d2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,14 +411,18 @@ export interface IMagicProperty {
}

export enum EStashType {
shared,
private,
D2R,
SSS,
CSTM,
D2X,
}

export interface IStash {
version: string;
type: EStashType;
pageCount: number;
itemCount: number;
shared: boolean;
sharedGold: number;
hardcore: boolean;
pages: IStashPage[];
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./d2/d2s";
export { readHeader, readHeaderData, writeHeader, writeHeaderData, fixHeader } from "./d2/header";
export { readAttributes, writeAttributes } from "./d2/attributes";
export { readSkills, writeSkills } from "./d2/skills";
export { read as readStash, write as writeStash } from "./d2/stash";
export { enhanceAttributes, enhanceItems, enhancePlayerAttributes } from "./d2/attribute_enhancer";
export { getConstantData, setConstantData } from "./d2/constants";
export * from "./data/parser";
Expand Down
23 changes: 22 additions & 1 deletion tests/d2/stash.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, should } from "chai";
import { expect } from "chai";
import { read, write } from "../../src/d2/stash";
import { constants } from "../../src/data/versions/96_constant_data";
import * as path from "path";
Expand Down Expand Up @@ -69,4 +69,25 @@ describe("stash", () => {
expect(buffer.length, "file size").to.eq(newBuffer.length);
expect(newJson, "json").to.deep.eq(jsonData);
});

it("should read D2X shared stash file (Atma/GoMule)", async () => {
const buffer = fs.readFileSync(path.join(__dirname, "../../examples/stash/Atma_GoMule.d2x"));
const jsonData = await read(buffer, version99.constants, 0x62);
expect(jsonData.version).to.eq("99");
expect(jsonData.itemCount).to.eq(2599);
expect(jsonData.pages[0]?.items?.length).to.eq(2599);
expect(jsonData.pages[0].items[2598].unique_name).to.eq("Twitchthroe");
});

it("should not write D2X shared stash file (Atma/GoMule)", async () => {
const buffer = fs.readFileSync(path.join(__dirname, "../../examples/stash/Atma_GoMule.d2x"));
const jsonData = await read(buffer, version99.constants, 0x62);
let err: unknown;
try {
await write(jsonData, constants, 0x62);
} catch (e) {
err = e;
}
expect(err).to.be.instanceOf(Error);
});
});