Skip to content

Commit

Permalink
feat: start parsing monster information
Browse files Browse the repository at this point in the history
  • Loading branch information
blacha committed Aug 31, 2020
1 parent a700f09 commit 8a66db3
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 62 deletions.
1 change: 1 addition & 0 deletions packages/bintools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { LangReader, LangNode } from './lang/lang.reader';
export { MonsterReader, MonsterNode } from './monster/monster.stat.reader';
10 changes: 7 additions & 3 deletions packages/bintools/src/monster/monster.stat.reader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { bp } from 'binparse';
import { bp, StrutInfer } from 'binparse';

const Monster = bp.object('Monster', {
const MonsterParser = bp.object('Monster', {
id: bp.lu16,
baseId: bp.lu16,
baseNextId: bp.lu16,
/** Name index, lookup inside of lang index */
name: bp.lu16,
/** Description lookup inside of lang index */
description: bp.lu16,
unk1: bp.lu16,
flags: bp.lu32,
code: bp.lu32,
unk100: bp.skip(424 - 20),
});

export type MonsterNode = StrutInfer<typeof MonsterParser>;

export const MonsterReader = bp.object('Monsters', {
count: bp.variable('count', bp.lu32),
monsters: bp.array('Monster', Monster, 'count'),
monsters: bp.array('Monster', MonsterParser, 'count'),
});
7 changes: 5 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Diablo2PacketFactory } from '@diablo2/packets';
import { PacketsPod } from '@diablo2/packets/build/packets-pod';
import { Diablo2GameSession } from './game.state';
import { Logger } from './log.interface';
import { Diablo2Lang, loadLangFiles } from './mpq/mpq.loader';
import { Diablo2MpqLang, Diablo2MpqMonsters, loadLangFiles, loadMonsterFiles } from './mpq/mpq.loader';

export class Diablo2Client {
lang: Diablo2Lang;
lang: Diablo2MpqLang;
monsters: Diablo2MpqMonsters;

clientToServer = new Diablo2PacketFactory();
serverToClient = new Diablo2PacketFactory();

Expand All @@ -17,6 +19,7 @@ export class Diablo2Client {
async init(path: string, logger: Logger): Promise<void> {
logger.info({ path }, 'Reading game data');
this.lang = await loadLangFiles(path, logger);
this.monsters = await loadMonsterFiles(path, this.lang, logger);
}

startSession(log: Logger): Diablo2GameSession {
Expand Down
34 changes: 0 additions & 34 deletions packages/core/src/game.state.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { ItemActionType, ItemQuality } from '@diablo2/data';
import * as P from '@diablo2/packets/build/packets-pod/server';
import { ulid } from 'ulid';
import { Diablo2Client } from './client';
import { Logger } from './log.interface';
import { Diablo2PacketParser } from './packet.parser';
import * as c from 'ansi-colors';

export class Diablo2GameSession {
id: string = ulid().toLowerCase();
Expand All @@ -17,37 +14,6 @@ export class Diablo2GameSession {
this.log = log;
this.client = client;
this.parser = new Diablo2PacketParser(client);
// this.parser.events.on('*', (pkt) => {
// // if (PacketIgnore.has(pkt.packet.id)) return;
// // if (PacketTrace.has(pkt.packet.id)) return console.log(pkt);
// // console.log(pkt, pkt.packet.name);
// });

this.parser.on(P.ItemActionWorld, (pkt) => {
if (pkt.action.id == ItemActionType.AddToGround) {
if (pkt.code == 'gld') return;
if (pkt.flags.isSimpleItem) return;

const obj = JSON.parse(JSON.stringify({ sockets: pkt.sockets, quality: pkt.quality?.name, def: pkt.defense }));
switch (pkt.quality.id) {
case ItemQuality.Inferior:
case ItemQuality.Normal:
case ItemQuality.Superior:
case ItemQuality.Magic:
case ItemQuality.Rare:
console.log(pkt.code, client.lang.get(pkt.code), obj);
break;
case ItemQuality.Set:
console.log(c.green(pkt.code), client.lang.get(pkt.code), obj);
break;
case ItemQuality.Unique:
console.log(c.yellow(pkt.code), client.lang.get(pkt.code), obj);
break;
}
} else {
// console.log(pkt);
}
});
}

onPacket(direction: 'in' | 'out', bytes: Buffer): void {
Expand Down
55 changes: 49 additions & 6 deletions packages/core/src/mpq/mpq.loader.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { LangNode, LangReader } from '@diablo2/bintools';
import { LangNode, LangReader, MonsterNode, MonsterReader } from '@diablo2/bintools';
import { promises as fs } from 'fs';
import * as path from 'path';
import { Logger } from '../log.interface';

export class Diablo2Lang {
export class Diablo2MpqLang {
map: Map<string, LangNode> = new Map();
index: LangNode[] = [];

Expand All @@ -20,25 +20,68 @@ export class Diablo2Lang {
}
}

export class Diablo2MpqMonsters {
lang: Diablo2MpqLang;
map: Map<number, MonsterNode> = new Map();

constructor(lang: Diablo2MpqLang) {
this.lang = lang;
}

add(mon: MonsterNode): void {
this.map.set(mon.id, mon);
}

get(monsterId: number): MonsterNode | undefined {
return this.map.get(monsterId);
}
}

/** Load all the `.tbl` files from a extracted MPQ to get item names */
export async function loadLangFiles(basePath: string, logger: Logger): Promise<Diablo2Lang> {
const lang = new Diablo2Lang();
export async function loadLangFiles(basePath: string, logger: Logger): Promise<Diablo2MpqLang> {
const lang = new Diablo2MpqLang();
if (basePath == null || basePath == '') return lang;

// TODO these lang files need to be extracted, would be good to load from the MPQ directly
const LangPath = `${basePath}/mpq/data/local/LNG/ENG`;
const langFolderFiles = await fs.readdir(LangPath);

const langFiles = langFolderFiles.filter((f) => LangReader.LangFiles.find((lf) => f.toLowerCase().startsWith(lf)));

for (const langFile of langFiles) {
const startTime = Date.now();
const bytes = await fs.readFile(path.join(LangPath, langFile));
const langItems = LangReader.parse(bytes);
const duration = Date.now() - startTime;

for (const itm of langItems) lang.add(itm);
logger.info({ file: langFile, records: langItems.length, duration }, 'Load language file');
logger.info({ file: langFile, records: langItems.length, duration }, 'MPQ:LoadLanguage');
}

return lang;
}

export async function loadMonsterFiles(
basePath: string,
lang: Diablo2MpqLang,
logger: Logger,
): Promise<Diablo2MpqMonsters> {
const mon = new Diablo2MpqMonsters(lang);
if (basePath == null || basePath == '') return mon;

// TODO these lang files need to be extracted, would be good to load from the MPQ directly
const BinPath = `${basePath}/mpq/data/global/excel`;
const binFolderFiles = await fs.readdir(BinPath);

const monStatFile = binFolderFiles.find((f) => f.toLowerCase() == 'monstats.bin');
if (monStatFile) {
const startTime = Date.now();
const bytes = await fs.readFile(path.join(BinPath, monStatFile));
const monItems = MonsterReader.raw(bytes as any);
const duration = Date.now() - startTime;

for (const monster of monItems.monsters) mon.add(monster);
logger.info({ file: monStatFile, records: monItems.monsters.length, duration }, 'MPQ:LoadMonster');
}

return mon;
}
34 changes: 34 additions & 0 deletions packages/packets/src/packets-pod/__tests__/npc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import o from 'ospec';
import { NpcAssign } from '../server';

o.spec('NpcAssign', () => {
o('should load a hungry dead', () => {
const npc = NpcAssign.parse(Buffer.from('acc400000006005514fd13801131400400', 'hex'), {
startOffset: 0,
offset: 1,
});
o(npc.unitId).equals(196);
o(npc.x).equals(5205);
o(npc.y).equals(5117);
o(npc.life).equals(128);
});

o('should parse a low health fallen', () => {
const npc = NpcAssign.parse(Buffer.from('ace70000001300b114e4102a1111c80000', 'hex'), {
startOffset: 0,
offset: 1,
});
o(npc.life).equals(42);
});
o('should parse super unique Rakanishu', () => {
const npc = NpcAssign.parse(Buffer.from('acc500000014003b14eb138019110480660020c2c002207219', 'hex'), {
startOffset: 0,
offset: 1,
});

o(npc.x).equals(5179);
o(npc.y).equals(5099);
o(npc.life).equals(128);
o(npc.code).equals(20);
});
});
2 changes: 2 additions & 0 deletions packages/packets/src/packets-pod/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,11 @@ export const StateEnd = Diablo2Packet.create(0xa9, 'StateEnd', {
export const NpcHeal = Diablo2Packet.create(0xab, 'NpcHeal', { unitType: bp.u8, unitId: bp.lu32, unitLife: bp.u8 });
export const NpcAssign = Diablo2Packet.create(0xac, 'NpcAssign', {
unitId: bp.lu32,
/** NpcId */
code: bp.lu16,
x: bp.lu16,
y: bp.lu16,
/** Health values are 0 - 128 */
life: bp.u8,
packetLength: bp.variable('count', bp.u8),
stateEffects: bp.array('StateEffects', bp.u8, 'count', true),
Expand Down
37 changes: 37 additions & 0 deletions packages/sniffer/src/example/item.tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Diablo2GameSession } from '@diablo2/core';
import { ItemActionType, ItemQuality } from '@diablo2/data';
import { ItemActionWorld } from '@diablo2/packets/build/packets-pod/server';
import * as c from 'ansi-colors';
import { Diablo2PacketSniffer } from '../sniffer';

/** Track all items dropped onto the ground */
export function sniffItems(sniffer: Diablo2PacketSniffer): void {
sniffer.onNewGame((game: Diablo2GameSession) => {
console.log('NewSessionStarted');
game.parser.on(ItemActionWorld, (pkt) => {
// Ignore items being moved around
if (pkt.action.id !== ItemActionType.AddToGround) return;
// Ignore gold
if (pkt.code == 'gld') return;
// Ignore things like scrolls/ health potion
if (pkt.flags.isSimpleItem) return;

const obj = JSON.parse(JSON.stringify({ sockets: pkt.sockets, quality: pkt.quality?.name, def: pkt.defense }));
switch (pkt.quality.id) {
case ItemQuality.Inferior:
case ItemQuality.Normal:
case ItemQuality.Superior:
case ItemQuality.Magic:
case ItemQuality.Rare:
console.log(pkt.code, sniffer.client.lang.get(pkt.code), obj);
break;
case ItemQuality.Set:
console.log(c.green(pkt.code), sniffer.client.lang.get(pkt.code), obj);
break;
case ItemQuality.Unique:
console.log(c.yellow(pkt.code), sniffer.client.lang.get(pkt.code), obj);
break;
}
});
});
}
15 changes: 15 additions & 0 deletions packages/sniffer/src/example/npc.tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Diablo2GameSession } from '@diablo2/core';
import { NpcAssign } from '@diablo2/packets/build/packets-pod/server';
import * as c from 'ansi-colors';
import { Diablo2PacketSniffer } from '../sniffer';

/** Track all NPCs that are being reported */
export function sniffNpc(sniffer: Diablo2PacketSniffer): void {
sniffer.onNewGame((game: Diablo2GameSession) => {
game.parser.on(NpcAssign, (npc) => {
const npcInfo = game.client.monsters.get(npc.code);
const monsterName = npcInfo == null ? 'Unknown' : game.client.lang.get(npcInfo.name);
console.log(c.red('Npc'), monsterName, `@ ${npc.x},${npc.y}`);
});
});
}
7 changes: 7 additions & 0 deletions packages/sniffer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { existsSync } from 'fs';
import 'source-map-support/register';
import { sniffItems } from './example/item.tracker';
import { sniffNpc } from './example/npc.tracker';
import { Log } from './logger';
import { Diablo2PacketSniffer, findLocalIps } from './sniffer';

Expand Down Expand Up @@ -41,6 +43,11 @@ async function main(): Promise<void> {
const sniffer = new Diablo2PacketSniffer(networkAdapter, gamePath);
sniffer.isWriteDump = isWriteDump > 0;

// Track items being dropped onto the ground
sniffItems(sniffer);

sniffNpc(sniffer);

await sniffer.start(Log);
}

Expand Down
30 changes: 30 additions & 0 deletions packages/sniffer/src/replay.tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createWriteStream, WriteStream } from 'fs';

/** Automatically close a write stream after 5 seconds of inactivity */
export class AutoClosingStream {
_stream: WriteStream | null;
_streamClose: NodeJS.Timer | null;
fileName: string;
closeTimeout: number;

constructor(fileName: string, closeTimeout = 5 * 1000) {
this.fileName = fileName;
this.closeTimeout = closeTimeout;
}

closeStream = (): void => {
this._stream?.close();
this._stream = null;
this._streamClose = null;
};

write(obj: unknown): void {
if (this._streamClose != null) clearTimeout(this._streamClose);

if (this._stream == null) {
this._stream = createWriteStream(this.fileName, { flags: 'a' });
}
this._streamClose = setTimeout(this.closeStream, 5 * 1000);
this._stream.write(JSON.stringify(obj) + '\n');
}
}
Loading

0 comments on commit 8a66db3

Please sign in to comment.