@astefanski/storm-parser 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @astefanski/storm-parser
2
+
3
+ A tool for parsing Heroes of the Storm (`.StormReplay`) replay files. Extract valuable match data including players, heroes, builds, match results, and more.
4
+
5
+ ## Installation
6
+
7
+ This is a private scoped package. Ensure you have access and are authenticated with npm, then install it via:
8
+
9
+ ```bash
10
+ npm install @astefanski/storm-parser
11
+ # or using pnpm
12
+ pnpm add @astefanski/storm-parser
13
+ # or using yarn
14
+ yarn add @astefanski/storm-parser
15
+ ```
16
+
17
+ > **Note:** During installation, a `postinstall` script automatically downloads protocol definitions from the [Blizzard/heroprotocol](https://github.com/Blizzard/heroprotocol) GitHub repository. This requires internet access and may take 1-2 minutes on first install.
18
+
19
+ ## Usage
20
+
21
+ The package exports two main classes: `ReplayParser` and `ReplayAnalyzer`.
22
+
23
+ ### ReplayParser
24
+
25
+ Used for reading the replay file and extracting the raw protocol data and events.
26
+
27
+ ```typescript
28
+ import { ReplayParser } from "@astefanski/storm-parser";
29
+ import * as fs from "fs";
30
+
31
+ // Read the replay file into a buffer
32
+ const replayBuffer = fs.readFileSync("path/to/your/replay.StormReplay");
33
+
34
+ // Initialize the parser
35
+ const parser = new ReplayParser(replayBuffer);
36
+ parser.init();
37
+
38
+ // Access the parsed replay data
39
+ console.log("Details:", parser.getDetails());
40
+ console.log("Init Data:", parser.getInitData());
41
+ console.log("Tracker Events Count:", parser.getTrackerEvents().length);
42
+ ```
43
+
44
+ ### ReplayAnalyzer
45
+
46
+ Used for higher-level analysis, transforming the raw replay data into structured match information, team compositions, and player stats.
47
+
48
+ ```typescript
49
+ import { ReplayAnalyzer } from "@astefanski/storm-parser";
50
+
51
+ const result = await ReplayAnalyzer.analyze("path/to/your/replay.StormReplay");
52
+
53
+ if (result.status === 1) {
54
+ console.log("Map Name:", result.match?.map);
55
+ console.log("Match Length (seconds):", result.match?.length);
56
+ console.log("Winning Team:", result.match?.winner);
57
+ }
58
+ ```
59
+
60
+ ## Features
61
+
62
+ - Parses `replay.details`, `replay.initData`, `replay.tracker.events`, and more.
63
+ - Extracts detailed player information, including BattleTags and selected heroes.
64
+ - Decodes tracker events for in-depth match analysis (e.g., score screens, talent choices).
65
+ - Protocols are downloaded on install — package stays lightweight (~15KB).
66
+ - Provides a clean, typed API for easy integration.
67
+
68
+ ## License
69
+
70
+ ISC
@@ -0,0 +1,74 @@
1
+ interface Protocol {
2
+ version: number;
3
+ typeinfos: any[];
4
+ game_event_types: Record<number, [number, string]>;
5
+ message_event_types: Record<number, [number, string]>;
6
+ tracker_event_types: Record<number, [number, string]>;
7
+ game_eventid_typeid: number;
8
+ message_eventid_typeid: number;
9
+ tracker_eventid_typeid: number;
10
+ svaruint32_typeid: number;
11
+ replay_userid_typeid: number;
12
+ replay_header_typeid: number;
13
+ game_details_typeid: number;
14
+ replay_initdata_typeid: number;
15
+ }
16
+ interface ReplayEvent {
17
+ _event: string;
18
+ _eventid: number;
19
+ _gameloop: number;
20
+ _userid?: unknown;
21
+ _bits: number;
22
+ [key: string]: unknown;
23
+ }
24
+ declare class ReplayParser {
25
+ private mpq;
26
+ private header;
27
+ private build;
28
+ private protocol?;
29
+ private baseProtocol?;
30
+ constructor(filenameOrData: string | Buffer);
31
+ init(): void;
32
+ private loadProtocolForBuild;
33
+ private decodeEventStream;
34
+ getDetails(): Record<string, unknown> | null;
35
+ getInitData(): Record<string, unknown> | null;
36
+ getTrackerEvents(): ReplayEvent[];
37
+ getGameEvents(): ReplayEvent[];
38
+ extractFile(filename: string): Buffer | null;
39
+ }
40
+
41
+ interface PlayerStat {
42
+ hero: string;
43
+ name: string;
44
+ tag: string;
45
+ team: number;
46
+ win: boolean;
47
+ gameStats: Record<string, number>;
48
+ }
49
+ interface TeamStat {
50
+ level: number;
51
+ takedowns: number;
52
+ ids: string[];
53
+ }
54
+ interface MatchStat {
55
+ map?: string;
56
+ date: string;
57
+ length: number;
58
+ winner: number;
59
+ version: {
60
+ m_build: number;
61
+ };
62
+ teams: Record<string, TeamStat>;
63
+ }
64
+ declare class ReplayAnalyzer {
65
+ static analyze(filePath: string): Promise<{
66
+ status: number;
67
+ match?: MatchStat;
68
+ players?: Record<string, PlayerStat>;
69
+ error?: string;
70
+ }>;
71
+ private static fileTimeToDate;
72
+ }
73
+
74
+ export { type Protocol, ReplayAnalyzer, type ReplayEvent, ReplayParser };
@@ -0,0 +1,74 @@
1
+ interface Protocol {
2
+ version: number;
3
+ typeinfos: any[];
4
+ game_event_types: Record<number, [number, string]>;
5
+ message_event_types: Record<number, [number, string]>;
6
+ tracker_event_types: Record<number, [number, string]>;
7
+ game_eventid_typeid: number;
8
+ message_eventid_typeid: number;
9
+ tracker_eventid_typeid: number;
10
+ svaruint32_typeid: number;
11
+ replay_userid_typeid: number;
12
+ replay_header_typeid: number;
13
+ game_details_typeid: number;
14
+ replay_initdata_typeid: number;
15
+ }
16
+ interface ReplayEvent {
17
+ _event: string;
18
+ _eventid: number;
19
+ _gameloop: number;
20
+ _userid?: unknown;
21
+ _bits: number;
22
+ [key: string]: unknown;
23
+ }
24
+ declare class ReplayParser {
25
+ private mpq;
26
+ private header;
27
+ private build;
28
+ private protocol?;
29
+ private baseProtocol?;
30
+ constructor(filenameOrData: string | Buffer);
31
+ init(): void;
32
+ private loadProtocolForBuild;
33
+ private decodeEventStream;
34
+ getDetails(): Record<string, unknown> | null;
35
+ getInitData(): Record<string, unknown> | null;
36
+ getTrackerEvents(): ReplayEvent[];
37
+ getGameEvents(): ReplayEvent[];
38
+ extractFile(filename: string): Buffer | null;
39
+ }
40
+
41
+ interface PlayerStat {
42
+ hero: string;
43
+ name: string;
44
+ tag: string;
45
+ team: number;
46
+ win: boolean;
47
+ gameStats: Record<string, number>;
48
+ }
49
+ interface TeamStat {
50
+ level: number;
51
+ takedowns: number;
52
+ ids: string[];
53
+ }
54
+ interface MatchStat {
55
+ map?: string;
56
+ date: string;
57
+ length: number;
58
+ winner: number;
59
+ version: {
60
+ m_build: number;
61
+ };
62
+ teams: Record<string, TeamStat>;
63
+ }
64
+ declare class ReplayAnalyzer {
65
+ static analyze(filePath: string): Promise<{
66
+ status: number;
67
+ match?: MatchStat;
68
+ players?: Record<string, PlayerStat>;
69
+ error?: string;
70
+ }>;
71
+ private static fileTimeToDate;
72
+ }
73
+
74
+ export { type Protocol, ReplayAnalyzer, type ReplayEvent, ReplayParser };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var $=Object.create;var P=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,G=Object.prototype.hasOwnProperty;var V=(i,e)=>{for(var t in e)P(i,t,{get:e[t],enumerable:!0})},N=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of j(e))!G.call(i,n)&&n!==t&&P(i,n,{get:()=>e[n],enumerable:!(r=C(e,n))||r.enumerable});return i};var M=(i,e,t)=>(t=i!=null?$(q(i)):{},N(e||!i||!i.__esModule?P(t,"default",{value:i,enumerable:!0}):t,i)),X=i=>N(P({},"__esModule",{value:!0}),i);var ne={};V(ne,{ReplayAnalyzer:()=>L,ReplayParser:()=>k});module.exports=X(ne);var F=M(require("fs")),S=M(require("zlib")),J=require("seek-bzip"),Q=512,K=65536,W=16777216,Y=67108864,Z=2147483648,ee={TABLE_OFFSET:0,HASH_A:1,HASH_B:2,TABLE:3};function te(){let i=1048577,e=new Uint32Array(256*5);for(let t=0;t<256;t++){let r=t;for(let n=0;n<5;n++){i=(i*125+3)%2796203;let s=(i&65535)<<16;i=(i*125+3)%2796203;let l=i&65535;e[r]=(s|l)>>>0,r+=256}}return e}var z=te(),R=class{file;header;hashTable;blockTable;files;constructor(e,t=!0){if(Buffer.isBuffer(e)?this.file=e:this.file=F.readFileSync(e),this.header=this.readHeader(),this.hashTable=this.readTable("hash"),this.blockTable=this.readTable("block"),t){let r=this.readFile("(listfile)");r?this.files=r.toString("utf8").trim().split(`\r
2
+ `):this.files=null}else this.files=null}readHeader(){let e=this.file.toString("utf8",0,4),t;if(e==="MPQ")t=this.readMPQHeader(),t.offset=0;else if(e==="MPQ\x1B"){let r=this.readMPQUserDataHeader();t=this.readMPQHeader(r.mpqHeaderOffset),t.offset=r.mpqHeaderOffset,t.userDataHeader=r}else throw new Error("Invalid MPQ file header");return t}readMPQHeader(e=0){let t=this.file.subarray(e,e+32),r={magic:t.toString("utf8",0,4),headerSize:t.readUInt32LE(4),archiveSize:t.readUInt32LE(8),formatVersion:t.readUInt16LE(12),sectorSizeShift:t.readUInt16LE(14),hashTableOffset:t.readUInt32LE(16),blockTableOffset:t.readUInt32LE(20),hashTableEntries:t.readUInt32LE(24),blockTableEntries:t.readUInt32LE(28)};if(r.formatVersion===1){let n=this.file.subarray(e+32,e+32+12);r.extendedBlockTableOffset=n.readUInt32LE(0)+n.readUInt32LE(4)*4294967296,r.hashTableOffsetHigh=n.readInt8(8),r.blockTableOffsetHigh=n.readInt8(10)}return r}readMPQUserDataHeader(){let e=this.file.subarray(0,16),t={magic:e.toString("utf8",0,4),userDataSize:e.readUInt32LE(4),mpqHeaderOffset:e.readUInt32LE(8),userDataHeaderSize:e.readUInt32LE(12)};return t.content=this.file.subarray(16,16+t.userDataHeaderSize),t}readTable(e){let t=e==="hash"?"hashTableOffset":"blockTableOffset",r=e==="hash"?"hashTableEntries":"blockTableEntries",n=this.header[t],s=this.header[r];if(n==null||s==null)throw new Error("Missing "+e+" offset or entries");let l=this.hash("("+e+" table)","TABLE"),a=this.file.subarray(n+(this.header.offset||0),n+(this.header.offset||0)+s*16);a=this.decrypt(a,l);let c=[];for(let _=0;_<s;_++){let h=a.subarray(_*16,_*16+16);e==="hash"?c.push({hashA:h.readUInt32LE(0),hashB:h.readUInt32LE(4),locale:h.readUInt16LE(8),platform:h.readUInt16LE(10),blockTableIndex:h.readUInt32LE(12)}):c.push({offset:h.readUInt32LE(0),archivedSize:h.readUInt32LE(4),size:h.readUInt32LE(8),flags:h.readUInt32LE(12)})}return c}getHashTableEntry(e){let t=this.hash(e,"HASH_A"),r=this.hash(e,"HASH_B");for(let n of this.hashTable)if(n.hashA===t&&n.hashB===r)return n}readFile(e,t=!1){function r(c){let _=c[0];if(_===0)return c;if(_===2)return S.inflateSync(c.subarray(1));if(_===16)return J.decode(c.subarray(1));try{return S.inflateSync(c.subarray(1))}catch{return S.inflateRawSync(c.subarray(1))}}let n=this.getHashTableEntry(e);if(!n)return null;let s=this.blockTable[n.blockTableIndex];if(!s||!(s.flags&Z))return null;if(s.archivedSize===0)return Buffer.alloc(0);let l=s.offset+(this.header.offset||0),a=this.file.subarray(l,l+s.archivedSize);if(s.flags&K)throw new Error("Encryption is not supported");if(s.flags&W)s.flags&Q&&(t||s.size>s.archivedSize)&&(a=r(a));else{let c=512<<this.header.sectorSizeShift,_=Math.trunc(s.size/c)+1,h=!1;s.flags&Y&&(h=!0,_+=1);let o=[];for(let d=0;d<_+1;d++)o.push(a.readUInt32LE(4*d));let u=o.length-(h?2:1),f=[],b=s.size;for(let d=0;d<u;d++){let m=a.subarray(o[d],o[d+1]);s.flags&Q&&(t||b>m.length)&&(m=r(m)),b-=m.length,f.push(m)}a=Buffer.concat(f)}return a}hash(e,t){let r=2146271213,n=4008636142;for(let s=0;s<e.length;s++){let l=e.toUpperCase().charCodeAt(s);r=(z[(ee[t]<<8)+l]^r+n)>>>0,n=l+r+n+(n<<5)+3>>>0}return r}decrypt(e,t){let r=t>>>0,n=4008636142,s=Buffer.alloc(e.length),l=e.length/4;for(let a=0;a<l;a++){n=n+z[1024+(r&255)]>>>0;let c=e.readUInt32LE(a*4);c=(c^r+n)>>>0,r=((~r<<21)+286331153|r>>>11)>>>0,n=c+n+(n<<5)+3>>>0,s.writeUInt32LE(c,a*4)}return s}};var B=class extends Error{constructor(e="Truncated Buffer"){super(e),this.name="TruncatedError"}},g=class extends Error{constructor(e="Corrupted Buffer"){super(e),this.name="CorruptedError"}},T=class{_data;_used;_next;_nextbits;_bigendian;constructor(e,t="big"){this._data=e,this._used=0,this._next=0,this._nextbits=0,this._bigendian=t==="big"}done(){return this._nextbits===0&&this._used>=this._data.length}used_bits(){return this._used*8-this._nextbits}byte_align(){this._nextbits=0}read_aligned_bytes(e){if(this.byte_align(),this._used+e>this._data.length)throw new B;let t=this._data.subarray(this._used,this._used+e);return this._used+=e,t}read_bits(e){let t=0,r=0;for(;r!==e;){if(this._nextbits===0){if(this.done())throw new B;this._next=this._data[this._used],this._used+=1,this._nextbits=8}let n=Math.min(e-r,this._nextbits),s=this._next&(1<<n)-1;this._bigendian?t+=s*Math.pow(2,e-r-n):t+=s*Math.pow(2,r),this._next>>=n,this._nextbits-=n,r+=n}return t}read_bits_bigint(e){let t=0n,r=0;for(;r!==e;){if(this._nextbits===0){if(this.done())throw new B;this._next=this._data[this._used],this._used+=1,this._nextbits=8}let n=Math.min(e-r,this._nextbits),s=BigInt(this._next&(1<<n)-1);this._bigendian?t|=s<<BigInt(e-r-n):t|=s<<BigInt(r),this._next>>=n,this._nextbits-=n,r+=n}return t}read_unaligned_bytes(e){let t=Buffer.alloc(e);for(let r=0;r<e;r++)t[r]=this.read_bits(8);return t}};var I=class{_buffer;_typeinfos;constructor(e,t){this._buffer=new T(e),this._typeinfos=t}instance(e){if(e>=this._typeinfos.length)throw new g(`Invalid typeid ${e}`);let t=this._typeinfos[e],r=t[0],n=t[1]||[],s=this[r];if(typeof s!="function")throw new Error(`Decoder method ${r} not implemented`);return s.apply(this,n)}byte_align(){this._buffer.byte_align()}done(){return this._buffer.done()}used_bits(){return this._buffer.used_bits()}_array(e,t){let r=this._int(e),n=new Array(r);for(let s=0;s<r;s++)n[s]=this.instance(t);return n}_bitarray(e){let t=this._int(e);return[t,this._buffer.read_bits(t)]}_blob(e){let t=this._int(e);return this._buffer.read_aligned_bytes(t)}_bool(){return this._int([0,1])!==0}_choice(e,t){let r=this._int(e);if(!(r in t))throw new g(`Choice tag ${r} not found`);let n=t[r];return{[n[0]]:this.instance(n[1])}}_fourcc(){let e=this._buffer.read_bits(32),t=Buffer.alloc(4);return t.writeUInt32BE(e,0),t.toString("ascii")}_int(e){return e[0]+this._buffer.read_bits(e[1])}_null(){return null}_optional(e){return this._bool()?this.instance(e):null}_real32(){return this._buffer.read_unaligned_bytes(4).readFloatBE(0)}_real64(){return this._buffer.read_unaligned_bytes(8).readDoubleBE(0)}_struct(e){let t={};for(let r of e)if(r[0]==="__parent"){let n=this.instance(r[1]);if(typeof n=="object"&&n!==null)t={...t,...n};else{if(e.length===1)return n;t[r[0]]=n}}else t[r[0]]=this.instance(r[1]);return t}},v=class{_buffer;_typeinfos;constructor(e,t){this._buffer=new T(e),this._typeinfos=t}instance(e){if(e>=this._typeinfos.length)throw new g(`Invalid typeid ${e}`);let t=this._typeinfos[e],r=t[0],n=t[1]||[],s=this[r];if(typeof s!="function")throw new Error(`Decoder method ${r} not implemented`);return s.apply(this,n)}byte_align(){this._buffer.byte_align()}done(){return this._buffer.done()}used_bits(){return this._buffer.used_bits()}_expect_skip(e){if(this._buffer.read_bits(8)!==e)throw new g(`Expected skip ${e}`)}_vint(){let e=this._buffer.read_bits(8),t=(e&1)!==0,r=BigInt(e>>1&63),n=6n;for(;(e&128)!==0;)e=this._buffer.read_bits(8),r|=BigInt(e&127)<<n,n+=7n;let s=t?-r:r;return s>=BigInt(Number.MIN_SAFE_INTEGER)&&s<=BigInt(Number.MAX_SAFE_INTEGER)?Number(s):s}_array(e,t){this._expect_skip(0);let r=Number(this._vint()),n=new Array(r);for(let s=0;s<r;s++)n[s]=this.instance(t);return n}_bitarray(e){this._expect_skip(1);let t=Number(this._vint());return[t,this._buffer.read_aligned_bytes(Math.floor((t+7)/8))]}_blob(e){this._expect_skip(2);let t=Number(this._vint());return this._buffer.read_aligned_bytes(t)}_bool(){return this._expect_skip(6),this._buffer.read_bits(8)!==0}_choice(e,t){this._expect_skip(3);let r=Number(this._vint());if(!(r in t))return this._skip_instance(),{};let n=t[r];return{[n[0]]:this.instance(n[1])}}_fourcc(){return this._expect_skip(7),this._buffer.read_aligned_bytes(4)}_int(e){return this._expect_skip(9),Number(this._vint())}_null(){return null}_optional(e){return this._expect_skip(4),this._buffer.read_bits(8)!==0?this.instance(e):null}_real32(){return this._expect_skip(7),this._buffer.read_aligned_bytes(4).readFloatBE(0)}_real64(){return this._expect_skip(8),this._buffer.read_aligned_bytes(8).readDoubleBE(0)}_struct(e){this._expect_skip(5);let t={},r=Number(this._vint());for(let n=0;n<r;n++){let s=Number(this._vint()),l=e.find(a=>a[2]===s);if(l)if(l[0]==="__parent"){let a=this.instance(l[1]);typeof a=="object"&&a!==null?t={...t,...a}:e.length===1?t=a:t[l[0]]=a}else t[l[0]]=this.instance(l[1]);else this._skip_instance()}return t}_skip_instance(){let e=this._buffer.read_bits(8);if(e===0){let t=Number(this._vint());for(let r=0;r<t;r++)this._skip_instance()}else if(e===1){let t=Number(this._vint());this._buffer.read_aligned_bytes(Math.floor((t+7)/8))}else if(e===2){let t=Number(this._vint());this._buffer.read_aligned_bytes(t)}else if(e===3)this._vint(),this._skip_instance();else if(e===4)this._buffer.read_bits(8)!==0&&this._skip_instance();else if(e===5){let t=Number(this._vint());for(let r=0;r<t;r++)this._vint(),this._skip_instance()}else e===6?this._buffer.read_aligned_bytes(1):e===7?this._buffer.read_aligned_bytes(4):e===8?this._buffer.read_aligned_bytes(8):e===9&&this._vint()}};var E=M(require("fs")),w=M(require("path"));function re(i){let e=i;for(;e!==w.default.dirname(e);){if(E.default.existsSync(w.default.join(e,"package.json")))return e;e=w.default.dirname(e)}throw new Error("@astefanski/storm-parser: Could not find package root (no package.json found)")}function A(){let i;try{i=__dirname}catch{i=process.cwd()}let e=re(i),t=w.default.join(e,"protocols");if(E.default.existsSync(t))return t;throw new Error("@astefanski/storm-parser: Protocols directory not found at "+t+". Did postinstall run? Try: npx tsx node_modules/@astefanski/storm-parser/scripts/postinstall.ts")}var U=new Map;function H(i){if(U.has(i))return U.get(i);let e=A(),t=w.default.join(e,`protocol${i}.json`);if(!E.default.existsSync(t))return null;let r=JSON.parse(E.default.readFileSync(t,"utf-8"));return U.set(i,r),r}function O(){let i=A();return E.default.readdirSync(i).filter(e=>/^protocol\d+\.json$/.test(e)).map(e=>parseInt(e.match(/\d+/)[0],10)).sort((e,t)=>e-t)}var k=class{mpq;header;build=0;protocol;baseProtocol;constructor(e){this.mpq=new R(e,!1)}init(){let e=H(29406);if(!e)throw new Error("Base protocol29406 not found. Did postinstall run? Try: npx tsx node_modules/@astefanski/storm-parser/scripts/postinstall.ts");this.baseProtocol=e;let t=this.mpq.header.userDataHeader;if(!t||!t.content)throw new Error("Replay does not have a user data header");let r=new v(t.content,this.baseProtocol.typeinfos);this.header=r.instance(this.baseProtocol.replay_header_typeid),this.build=this.header.m_version.m_baseBuild,this.protocol=this.loadProtocolForBuild(this.build)}loadProtocolForBuild(e){let t=H(e);if(!t){let r=O(),n=r[0];for(let s of r)s<=e&&s>n&&(n=s);t=H(n)}if(!t)throw new Error(`No protocol found for build ${e}`);return t}*decodeEventStream(e,t,r,n){if(!this.protocol)throw new Error("Protocol not loaded");let s=0;for(;!e.done();){let l=e.used_bits(),a=e.instance(this.protocol.svaruint32_typeid),c=Object.keys(a)[0],_=a[c];s+=_;let h=n?e.instance(this.protocol.replay_userid_typeid):void 0,o=Number(e.instance(t)),u=r[o];if(!u)throw new Error(`Unknown eventid(${o})`);let f=u[0],b=u[1],d=e.instance(f);d._event=b,d._eventid=o,d._gameloop=s,n&&(d._userid=h),e.byte_align(),d._bits=e.used_bits()-l,yield d}}getDetails(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.details");return e?new v(e,this.protocol.typeinfos).instance(this.protocol.game_details_typeid):null}getInitData(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.initData");return e?new I(e,this.protocol.typeinfos).instance(this.protocol.replay_initdata_typeid):null}getTrackerEvents(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.tracker.events");if(!e)return[];let t=new v(e,this.protocol.typeinfos);return Array.from(this.decodeEventStream(t,this.protocol.tracker_eventid_typeid,this.protocol.tracker_event_types,!1))}getGameEvents(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.game.events");if(!e)return[];let t=new I(e,this.protocol.typeinfos);return Array.from(this.decodeEventStream(t,this.protocol.game_eventid_typeid,this.protocol.game_event_types,!0))}extractFile(e){return this.mpq.readFile(e)}};var L=class{static async analyze(e){try{let t=new k(e);await t.init();let r=t.getDetails(),n=t.getTrackerEvents();if(!r)throw new Error("Missing replay.details from parsed MPQ archive");console.log("Raw TimeUTC:",r?.m_timeUTC,"Type:",typeof r?.m_timeUTC);let s={map:r?.m_title?.toString("utf8"),date:r?this.fileTimeToDate(r.m_timeUTC).toISOString():new Date().toISOString(),length:0,winner:-1,version:{m_build:t.build},teams:{0:{level:0,takedowns:0,ids:[]},1:{level:0,takedowns:0,ids:[]}}},l={};for(let o of r.m_playerList){if(!o||!o.m_toon)continue;let u=o.m_toon,f=`${u.m_region}-${u.m_programId}-${u.m_realm}-${u.m_id}`;l[f]={hero:o.m_hero?.toString("utf8")||"",name:o.m_name?.toString("utf8")||"",tag:"",team:o.m_teamId,win:o.m_result===1,gameStats:{}}}let a=t.extractFile("replay.server.battlelobby");if(a)try{let o=new RegExp("([\\p{L}\\d]{3,24}#\\d{4,10})[z\xD8]?","gu"),u=a.toString("utf8").match(o);if(u){let f=0;for(let b of r.m_playerList){if(!b||!b.m_toon)continue;let d=b.m_name?.toString("utf8");for(;f<u.length;){let p=u[f].split("#"),D=p[0],x=p[1].replace(/[zØ]/g,"");if(f++,D===d){let y=`${b.m_toon.m_region}-${b.m_toon.m_programId}-${b.m_toon.m_realm}-${b.m_toon.m_id}`;l[y]&&(l[y].tag=x);break}}}}}catch(o){console.error("BattleTag regex error:",o)}let c={},_=0,h=0;for(let o of n){if(o._event==="NNet.Replay.Tracker.SStatGameEvent"){let u=o.m_eventName?.toString("utf8");if(u==="PlayerInit"){let f=o.m_intData,b=o.m_stringData,d=f[0].m_value,m=b[1].m_value?.toString("utf8");m&&l[m]&&(c[d]=m)}else u==="GatesOpen"&&(_=o._gameloop)}h=Math.max(h,o._gameloop)}s.length=Math.floor((h-_)/16);for(let o of n)if(o._event==="NNet.Replay.Tracker.SScoreResultEvent"){let u=o.m_instanceList;for(let f of u){let b=f.m_name?.toString("utf8"),d=f.m_values,m=0;if(!b.startsWith("EndOfMatchAward"))for(let p of d)if(p&&p.length>0&&p[0]!==void 0){let D=m+1,x=c[D];if(x&&l[x]){let y=typeof p[0]=="object"&&p[0]!==null&&"m_value"in p[0]?p[0].m_value:p[0];y!=null&&(l[x].gameStats[b]=y)}m++}else p&&p.length===0&&m++}}for(let[o,u]of Object.entries(l)){u.win&&(s.winner=u.team);let f=s.teams[u.team.toString()];f&&(f.level=Math.max(f.level,u.gameStats.Level||0),f.takedowns+=u.gameStats.Takedowns||0,f.ids.push(o))}return{status:1,match:s,players:l}}catch(t){return console.error("ReplayAnalyzer Error:",t),{status:-2,error:String(t)}}}static fileTimeToDate(e){return new Date(Number(e)/1e4-116444736e5)}};0&&(module.exports={ReplayAnalyzer,ReplayParser});
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var F=(i=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(i,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):i)(function(i){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+i+'" is not supported')});import*as N from"fs";import*as E from"zlib";var A=F("seek-bzip"),D=512,O=65536,$=16777216,C=67108864,j=2147483648,q={TABLE_OFFSET:0,HASH_A:1,HASH_B:2,TABLE:3};function G(){let i=1048577,e=new Uint32Array(256*5);for(let t=0;t<256;t++){let r=t;for(let n=0;n<5;n++){i=(i*125+3)%2796203;let s=(i&65535)<<16;i=(i*125+3)%2796203;let l=i&65535;e[r]=(s|l)>>>0,r+=256}}return e}var U=G(),P=class{file;header;hashTable;blockTable;files;constructor(e,t=!0){if(Buffer.isBuffer(e)?this.file=e:this.file=N.readFileSync(e),this.header=this.readHeader(),this.hashTable=this.readTable("hash"),this.blockTable=this.readTable("block"),t){let r=this.readFile("(listfile)");r?this.files=r.toString("utf8").trim().split(`\r
2
+ `):this.files=null}else this.files=null}readHeader(){let e=this.file.toString("utf8",0,4),t;if(e==="MPQ")t=this.readMPQHeader(),t.offset=0;else if(e==="MPQ\x1B"){let r=this.readMPQUserDataHeader();t=this.readMPQHeader(r.mpqHeaderOffset),t.offset=r.mpqHeaderOffset,t.userDataHeader=r}else throw new Error("Invalid MPQ file header");return t}readMPQHeader(e=0){let t=this.file.subarray(e,e+32),r={magic:t.toString("utf8",0,4),headerSize:t.readUInt32LE(4),archiveSize:t.readUInt32LE(8),formatVersion:t.readUInt16LE(12),sectorSizeShift:t.readUInt16LE(14),hashTableOffset:t.readUInt32LE(16),blockTableOffset:t.readUInt32LE(20),hashTableEntries:t.readUInt32LE(24),blockTableEntries:t.readUInt32LE(28)};if(r.formatVersion===1){let n=this.file.subarray(e+32,e+32+12);r.extendedBlockTableOffset=n.readUInt32LE(0)+n.readUInt32LE(4)*4294967296,r.hashTableOffsetHigh=n.readInt8(8),r.blockTableOffsetHigh=n.readInt8(10)}return r}readMPQUserDataHeader(){let e=this.file.subarray(0,16),t={magic:e.toString("utf8",0,4),userDataSize:e.readUInt32LE(4),mpqHeaderOffset:e.readUInt32LE(8),userDataHeaderSize:e.readUInt32LE(12)};return t.content=this.file.subarray(16,16+t.userDataHeaderSize),t}readTable(e){let t=e==="hash"?"hashTableOffset":"blockTableOffset",r=e==="hash"?"hashTableEntries":"blockTableEntries",n=this.header[t],s=this.header[r];if(n==null||s==null)throw new Error("Missing "+e+" offset or entries");let l=this.hash("("+e+" table)","TABLE"),a=this.file.subarray(n+(this.header.offset||0),n+(this.header.offset||0)+s*16);a=this.decrypt(a,l);let c=[];for(let _=0;_<s;_++){let h=a.subarray(_*16,_*16+16);e==="hash"?c.push({hashA:h.readUInt32LE(0),hashB:h.readUInt32LE(4),locale:h.readUInt16LE(8),platform:h.readUInt16LE(10),blockTableIndex:h.readUInt32LE(12)}):c.push({offset:h.readUInt32LE(0),archivedSize:h.readUInt32LE(4),size:h.readUInt32LE(8),flags:h.readUInt32LE(12)})}return c}getHashTableEntry(e){let t=this.hash(e,"HASH_A"),r=this.hash(e,"HASH_B");for(let n of this.hashTable)if(n.hashA===t&&n.hashB===r)return n}readFile(e,t=!1){function r(c){let _=c[0];if(_===0)return c;if(_===2)return E.inflateSync(c.subarray(1));if(_===16)return A.decode(c.subarray(1));try{return E.inflateSync(c.subarray(1))}catch{return E.inflateRawSync(c.subarray(1))}}let n=this.getHashTableEntry(e);if(!n)return null;let s=this.blockTable[n.blockTableIndex];if(!s||!(s.flags&j))return null;if(s.archivedSize===0)return Buffer.alloc(0);let l=s.offset+(this.header.offset||0),a=this.file.subarray(l,l+s.archivedSize);if(s.flags&O)throw new Error("Encryption is not supported");if(s.flags&$)s.flags&D&&(t||s.size>s.archivedSize)&&(a=r(a));else{let c=512<<this.header.sectorSizeShift,_=Math.trunc(s.size/c)+1,h=!1;s.flags&C&&(h=!0,_+=1);let o=[];for(let d=0;d<_+1;d++)o.push(a.readUInt32LE(4*d));let u=o.length-(h?2:1),f=[],b=s.size;for(let d=0;d<u;d++){let m=a.subarray(o[d],o[d+1]);s.flags&D&&(t||b>m.length)&&(m=r(m)),b-=m.length,f.push(m)}a=Buffer.concat(f)}return a}hash(e,t){let r=2146271213,n=4008636142;for(let s=0;s<e.length;s++){let l=e.toUpperCase().charCodeAt(s);r=(U[(q[t]<<8)+l]^r+n)>>>0,n=l+r+n+(n<<5)+3>>>0}return r}decrypt(e,t){let r=t>>>0,n=4008636142,s=Buffer.alloc(e.length),l=e.length/4;for(let a=0;a<l;a++){n=n+U[1024+(r&255)]>>>0;let c=e.readUInt32LE(a*4);c=(c^r+n)>>>0,r=((~r<<21)+286331153|r>>>11)>>>0,n=c+n+(n<<5)+3>>>0,s.writeUInt32LE(c,a*4)}return s}};var k=class extends Error{constructor(e="Truncated Buffer"){super(e),this.name="TruncatedError"}},g=class extends Error{constructor(e="Corrupted Buffer"){super(e),this.name="CorruptedError"}},x=class{_data;_used;_next;_nextbits;_bigendian;constructor(e,t="big"){this._data=e,this._used=0,this._next=0,this._nextbits=0,this._bigendian=t==="big"}done(){return this._nextbits===0&&this._used>=this._data.length}used_bits(){return this._used*8-this._nextbits}byte_align(){this._nextbits=0}read_aligned_bytes(e){if(this.byte_align(),this._used+e>this._data.length)throw new k;let t=this._data.subarray(this._used,this._used+e);return this._used+=e,t}read_bits(e){let t=0,r=0;for(;r!==e;){if(this._nextbits===0){if(this.done())throw new k;this._next=this._data[this._used],this._used+=1,this._nextbits=8}let n=Math.min(e-r,this._nextbits),s=this._next&(1<<n)-1;this._bigendian?t+=s*Math.pow(2,e-r-n):t+=s*Math.pow(2,r),this._next>>=n,this._nextbits-=n,r+=n}return t}read_bits_bigint(e){let t=0n,r=0;for(;r!==e;){if(this._nextbits===0){if(this.done())throw new k;this._next=this._data[this._used],this._used+=1,this._nextbits=8}let n=Math.min(e-r,this._nextbits),s=BigInt(this._next&(1<<n)-1);this._bigendian?t|=s<<BigInt(e-r-n):t|=s<<BigInt(r),this._next>>=n,this._nextbits-=n,r+=n}return t}read_unaligned_bytes(e){let t=Buffer.alloc(e);for(let r=0;r<e;r++)t[r]=this.read_bits(8);return t}};var S=class{_buffer;_typeinfos;constructor(e,t){this._buffer=new x(e),this._typeinfos=t}instance(e){if(e>=this._typeinfos.length)throw new g(`Invalid typeid ${e}`);let t=this._typeinfos[e],r=t[0],n=t[1]||[],s=this[r];if(typeof s!="function")throw new Error(`Decoder method ${r} not implemented`);return s.apply(this,n)}byte_align(){this._buffer.byte_align()}done(){return this._buffer.done()}used_bits(){return this._buffer.used_bits()}_array(e,t){let r=this._int(e),n=new Array(r);for(let s=0;s<r;s++)n[s]=this.instance(t);return n}_bitarray(e){let t=this._int(e);return[t,this._buffer.read_bits(t)]}_blob(e){let t=this._int(e);return this._buffer.read_aligned_bytes(t)}_bool(){return this._int([0,1])!==0}_choice(e,t){let r=this._int(e);if(!(r in t))throw new g(`Choice tag ${r} not found`);let n=t[r];return{[n[0]]:this.instance(n[1])}}_fourcc(){let e=this._buffer.read_bits(32),t=Buffer.alloc(4);return t.writeUInt32BE(e,0),t.toString("ascii")}_int(e){return e[0]+this._buffer.read_bits(e[1])}_null(){return null}_optional(e){return this._bool()?this.instance(e):null}_real32(){return this._buffer.read_unaligned_bytes(4).readFloatBE(0)}_real64(){return this._buffer.read_unaligned_bytes(8).readDoubleBE(0)}_struct(e){let t={};for(let r of e)if(r[0]==="__parent"){let n=this.instance(r[1]);if(typeof n=="object"&&n!==null)t={...t,...n};else{if(e.length===1)return n;t[r[0]]=n}}else t[r[0]]=this.instance(r[1]);return t}},v=class{_buffer;_typeinfos;constructor(e,t){this._buffer=new x(e),this._typeinfos=t}instance(e){if(e>=this._typeinfos.length)throw new g(`Invalid typeid ${e}`);let t=this._typeinfos[e],r=t[0],n=t[1]||[],s=this[r];if(typeof s!="function")throw new Error(`Decoder method ${r} not implemented`);return s.apply(this,n)}byte_align(){this._buffer.byte_align()}done(){return this._buffer.done()}used_bits(){return this._buffer.used_bits()}_expect_skip(e){if(this._buffer.read_bits(8)!==e)throw new g(`Expected skip ${e}`)}_vint(){let e=this._buffer.read_bits(8),t=(e&1)!==0,r=BigInt(e>>1&63),n=6n;for(;(e&128)!==0;)e=this._buffer.read_bits(8),r|=BigInt(e&127)<<n,n+=7n;let s=t?-r:r;return s>=BigInt(Number.MIN_SAFE_INTEGER)&&s<=BigInt(Number.MAX_SAFE_INTEGER)?Number(s):s}_array(e,t){this._expect_skip(0);let r=Number(this._vint()),n=new Array(r);for(let s=0;s<r;s++)n[s]=this.instance(t);return n}_bitarray(e){this._expect_skip(1);let t=Number(this._vint());return[t,this._buffer.read_aligned_bytes(Math.floor((t+7)/8))]}_blob(e){this._expect_skip(2);let t=Number(this._vint());return this._buffer.read_aligned_bytes(t)}_bool(){return this._expect_skip(6),this._buffer.read_bits(8)!==0}_choice(e,t){this._expect_skip(3);let r=Number(this._vint());if(!(r in t))return this._skip_instance(),{};let n=t[r];return{[n[0]]:this.instance(n[1])}}_fourcc(){return this._expect_skip(7),this._buffer.read_aligned_bytes(4)}_int(e){return this._expect_skip(9),Number(this._vint())}_null(){return null}_optional(e){return this._expect_skip(4),this._buffer.read_bits(8)!==0?this.instance(e):null}_real32(){return this._expect_skip(7),this._buffer.read_aligned_bytes(4).readFloatBE(0)}_real64(){return this._expect_skip(8),this._buffer.read_aligned_bytes(8).readDoubleBE(0)}_struct(e){this._expect_skip(5);let t={},r=Number(this._vint());for(let n=0;n<r;n++){let s=Number(this._vint()),l=e.find(a=>a[2]===s);if(l)if(l[0]==="__parent"){let a=this.instance(l[1]);typeof a=="object"&&a!==null?t={...t,...a}:e.length===1?t=a:t[l[0]]=a}else t[l[0]]=this.instance(l[1]);else this._skip_instance()}return t}_skip_instance(){let e=this._buffer.read_bits(8);if(e===0){let t=Number(this._vint());for(let r=0;r<t;r++)this._skip_instance()}else if(e===1){let t=Number(this._vint());this._buffer.read_aligned_bytes(Math.floor((t+7)/8))}else if(e===2){let t=Number(this._vint());this._buffer.read_aligned_bytes(t)}else if(e===3)this._vint(),this._skip_instance();else if(e===4)this._buffer.read_bits(8)!==0&&this._skip_instance();else if(e===5){let t=Number(this._vint());for(let r=0;r<t;r++)this._vint(),this._skip_instance()}else e===6?this._buffer.read_aligned_bytes(1):e===7?this._buffer.read_aligned_bytes(4):e===8?this._buffer.read_aligned_bytes(8):e===9&&this._vint()}};import T from"fs";import B from"path";function V(i){let e=i;for(;e!==B.dirname(e);){if(T.existsSync(B.join(e,"package.json")))return e;e=B.dirname(e)}throw new Error("@astefanski/storm-parser: Could not find package root (no package.json found)")}function Q(){let i;try{i=__dirname}catch{i=process.cwd()}let e=V(i),t=B.join(e,"protocols");if(T.existsSync(t))return t;throw new Error("@astefanski/storm-parser: Protocols directory not found at "+t+". Did postinstall run? Try: npx tsx node_modules/@astefanski/storm-parser/scripts/postinstall.ts")}var H=new Map;function M(i){if(H.has(i))return H.get(i);let e=Q(),t=B.join(e,`protocol${i}.json`);if(!T.existsSync(t))return null;let r=JSON.parse(T.readFileSync(t,"utf-8"));return H.set(i,r),r}function z(){let i=Q();return T.readdirSync(i).filter(e=>/^protocol\d+\.json$/.test(e)).map(e=>parseInt(e.match(/\d+/)[0],10)).sort((e,t)=>e-t)}var I=class{mpq;header;build=0;protocol;baseProtocol;constructor(e){this.mpq=new P(e,!1)}init(){let e=M(29406);if(!e)throw new Error("Base protocol29406 not found. Did postinstall run? Try: npx tsx node_modules/@astefanski/storm-parser/scripts/postinstall.ts");this.baseProtocol=e;let t=this.mpq.header.userDataHeader;if(!t||!t.content)throw new Error("Replay does not have a user data header");let r=new v(t.content,this.baseProtocol.typeinfos);this.header=r.instance(this.baseProtocol.replay_header_typeid),this.build=this.header.m_version.m_baseBuild,this.protocol=this.loadProtocolForBuild(this.build)}loadProtocolForBuild(e){let t=M(e);if(!t){let r=z(),n=r[0];for(let s of r)s<=e&&s>n&&(n=s);t=M(n)}if(!t)throw new Error(`No protocol found for build ${e}`);return t}*decodeEventStream(e,t,r,n){if(!this.protocol)throw new Error("Protocol not loaded");let s=0;for(;!e.done();){let l=e.used_bits(),a=e.instance(this.protocol.svaruint32_typeid),c=Object.keys(a)[0],_=a[c];s+=_;let h=n?e.instance(this.protocol.replay_userid_typeid):void 0,o=Number(e.instance(t)),u=r[o];if(!u)throw new Error(`Unknown eventid(${o})`);let f=u[0],b=u[1],d=e.instance(f);d._event=b,d._eventid=o,d._gameloop=s,n&&(d._userid=h),e.byte_align(),d._bits=e.used_bits()-l,yield d}}getDetails(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.details");return e?new v(e,this.protocol.typeinfos).instance(this.protocol.game_details_typeid):null}getInitData(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.initData");return e?new S(e,this.protocol.typeinfos).instance(this.protocol.replay_initdata_typeid):null}getTrackerEvents(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.tracker.events");if(!e)return[];let t=new v(e,this.protocol.typeinfos);return Array.from(this.decodeEventStream(t,this.protocol.tracker_eventid_typeid,this.protocol.tracker_event_types,!1))}getGameEvents(){if(!this.protocol)throw new Error("Protocol not loaded");let e=this.mpq.readFile("replay.game.events");if(!e)return[];let t=new S(e,this.protocol.typeinfos);return Array.from(this.decodeEventStream(t,this.protocol.game_eventid_typeid,this.protocol.game_event_types,!0))}extractFile(e){return this.mpq.readFile(e)}};var L=class{static async analyze(e){try{let t=new I(e);await t.init();let r=t.getDetails(),n=t.getTrackerEvents();if(!r)throw new Error("Missing replay.details from parsed MPQ archive");console.log("Raw TimeUTC:",r?.m_timeUTC,"Type:",typeof r?.m_timeUTC);let s={map:r?.m_title?.toString("utf8"),date:r?this.fileTimeToDate(r.m_timeUTC).toISOString():new Date().toISOString(),length:0,winner:-1,version:{m_build:t.build},teams:{0:{level:0,takedowns:0,ids:[]},1:{level:0,takedowns:0,ids:[]}}},l={};for(let o of r.m_playerList){if(!o||!o.m_toon)continue;let u=o.m_toon,f=`${u.m_region}-${u.m_programId}-${u.m_realm}-${u.m_id}`;l[f]={hero:o.m_hero?.toString("utf8")||"",name:o.m_name?.toString("utf8")||"",tag:"",team:o.m_teamId,win:o.m_result===1,gameStats:{}}}let a=t.extractFile("replay.server.battlelobby");if(a)try{let o=new RegExp("([\\p{L}\\d]{3,24}#\\d{4,10})[z\xD8]?","gu"),u=a.toString("utf8").match(o);if(u){let f=0;for(let b of r.m_playerList){if(!b||!b.m_toon)continue;let d=b.m_name?.toString("utf8");for(;f<u.length;){let p=u[f].split("#"),R=p[0],w=p[1].replace(/[zØ]/g,"");if(f++,R===d){let y=`${b.m_toon.m_region}-${b.m_toon.m_programId}-${b.m_toon.m_realm}-${b.m_toon.m_id}`;l[y]&&(l[y].tag=w);break}}}}}catch(o){console.error("BattleTag regex error:",o)}let c={},_=0,h=0;for(let o of n){if(o._event==="NNet.Replay.Tracker.SStatGameEvent"){let u=o.m_eventName?.toString("utf8");if(u==="PlayerInit"){let f=o.m_intData,b=o.m_stringData,d=f[0].m_value,m=b[1].m_value?.toString("utf8");m&&l[m]&&(c[d]=m)}else u==="GatesOpen"&&(_=o._gameloop)}h=Math.max(h,o._gameloop)}s.length=Math.floor((h-_)/16);for(let o of n)if(o._event==="NNet.Replay.Tracker.SScoreResultEvent"){let u=o.m_instanceList;for(let f of u){let b=f.m_name?.toString("utf8"),d=f.m_values,m=0;if(!b.startsWith("EndOfMatchAward"))for(let p of d)if(p&&p.length>0&&p[0]!==void 0){let R=m+1,w=c[R];if(w&&l[w]){let y=typeof p[0]=="object"&&p[0]!==null&&"m_value"in p[0]?p[0].m_value:p[0];y!=null&&(l[w].gameStats[b]=y)}m++}else p&&p.length===0&&m++}}for(let[o,u]of Object.entries(l)){u.win&&(s.winner=u.team);let f=s.teams[u.team.toString()];f&&(f.level=Math.max(f.level,u.gameStats.Level||0),f.takedowns+=u.gameStats.Takedowns||0,f.ids.push(o))}return{status:1,match:s,players:l}}catch(t){return console.error("ReplayAnalyzer Error:",t),{status:-2,error:String(t)}}}static fileTimeToDate(e){return new Date(Number(e)/1e4-116444736e5)}};export{L as ReplayAnalyzer,I as ReplayParser};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@astefanski/storm-parser",
3
+ "version": "0.0.1",
4
+ "description": "Storm Parser is a tool for parsing Heroes of the Storm replays.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "scripts"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [],
28
+ "author": {
29
+ "name": "Aleksander Stefański",
30
+ "email": "aleksander.stefanski@outlook.com"
31
+ },
32
+ "license": "ISC",
33
+ "devDependencies": {
34
+ "@eslint/js": "^10.0.1",
35
+ "@types/node": "^25.3.0",
36
+ "eslint": "^10.0.1",
37
+ "globals": "^17.3.0",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^5.9.3",
40
+ "typescript-eslint": "^8.56.0",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "dependencies": {
44
+ "seek-bzip": "^2.0.0",
45
+ "tsx": "^4.21.0"
46
+ },
47
+ "scripts": {
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "lint": "eslint . --cache",
51
+ "lint:fix": "eslint . --fix --cache",
52
+ "build": "tsup src/index.ts --format cjs,esm --dts --minify --clean --no-splitting",
53
+ "postinstall": "tsx scripts/postinstall.ts",
54
+ "generate:protocols": "tsx scripts/postinstall.ts"
55
+ }
56
+ }
@@ -0,0 +1,191 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import https from "https";
4
+ import { IncomingMessage } from "http";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const PROTOCOLS_LIST_URL =
11
+ "https://api.github.com/repos/Blizzard/heroprotocol/contents/heroprotocol/versions";
12
+ const RAW_BASE_URL =
13
+ "https://raw.githubusercontent.com/Blizzard/heroprotocol/master/heroprotocol/versions/";
14
+ const PROTOCOL_REGEX = /protocol(\d+)\.py$/;
15
+
16
+ interface GitHubFile {
17
+ name: string;
18
+ }
19
+
20
+ const fetchJson = async (url: string): Promise<unknown> => {
21
+ return new Promise((resolve, reject) => {
22
+ https
23
+ .get(
24
+ url,
25
+ { headers: { "User-Agent": "storm-parser-postinstall" } },
26
+ (res: IncomingMessage) => {
27
+ let data = "";
28
+ res.on("data", (chunk: string | Buffer) => (data += chunk));
29
+ res.on("end", () => resolve(JSON.parse(data)));
30
+ },
31
+ )
32
+ .on("error", reject);
33
+ });
34
+ };
35
+
36
+ const fetchText = async (url: string): Promise<string> => {
37
+ return new Promise((resolve, reject) => {
38
+ https
39
+ .get(url, (res: IncomingMessage) => {
40
+ let data = "";
41
+ res.on("data", (chunk: string | Buffer) => (data += chunk));
42
+ res.on("end", () => resolve(data));
43
+ })
44
+ .on("error", reject);
45
+ });
46
+ };
47
+
48
+ function pyToJson(pyStr: string): string {
49
+ let s = pyStr;
50
+ s = s.replace(/\(/g, "[").replace(/\)/g, "]");
51
+ s = s.replace(/\bTrue\b/g, "true");
52
+ s = s.replace(/\bFalse\b/g, "false");
53
+ s = s.replace(/\bNone\b/g, "null");
54
+ s = s.replace(/'/g, '"');
55
+ s = s.replace(/([{,]\s*)(\d+)\s*:/g, '$1"$2":');
56
+ s = s.replace(/#.*/g, "");
57
+ s = s.replace(/,\s*([\]}])/g, "$1");
58
+ return s;
59
+ }
60
+
61
+ function extractVariable(code: string, varName: string): unknown {
62
+ const regex = new RegExp(
63
+ `${varName}\\s*=\\s*([\\[\\{].*?[\\]\\}])(?:\\n\\S|\\n\\n|$)`,
64
+ "s",
65
+ );
66
+ const match = code.match(regex);
67
+ if (!match) {
68
+ const numMatch = code.match(new RegExp(`${varName}\\s*=\\s*(\\d+)`, "s"));
69
+ if (numMatch) return parseInt(numMatch[1], 10);
70
+ return null;
71
+ }
72
+
73
+ const blockEnd = code.indexOf("\n\n", code.indexOf(`${varName} = `));
74
+ let block = code.substring(
75
+ code.indexOf(`${varName} = `) + `${varName} = `.length,
76
+ blockEnd > -1 ? blockEnd : code.length,
77
+ );
78
+ if (!block.trim().endsWith("}") && !block.trim().endsWith("]")) {
79
+ const lastBrace = Math.max(block.lastIndexOf("}"), block.lastIndexOf("]"));
80
+ block = block.substring(0, lastBrace + 1);
81
+ }
82
+
83
+ try {
84
+ return JSON.parse(pyToJson(block));
85
+ } catch {
86
+ try {
87
+
88
+ return eval("(" + pyToJson(block) + ")");
89
+ } catch (e2) {
90
+ console.error(`Failed to parse ${varName}`, e2);
91
+ return null;
92
+ }
93
+ }
94
+ }
95
+
96
+ async function main() {
97
+ // Determine the output directory relative to the package root
98
+ const packageRoot = path.resolve(__dirname, "..");
99
+ const outDir = path.join(packageRoot, "protocols");
100
+
101
+ // Skip if protocols already exist
102
+ if (fs.existsSync(outDir)) {
103
+ const existing = fs.readdirSync(outDir).filter((f) => f.endsWith(".json"));
104
+ if (existing.length > 0) {
105
+ console.log(
106
+ `@astefanski/storm-parser: ${existing.length} protocols already present, skipping download.`,
107
+ );
108
+ return;
109
+ }
110
+ }
111
+
112
+ fs.mkdirSync(outDir, { recursive: true });
113
+
114
+ console.log(
115
+ "@astefanski/storm-parser: Downloading protocols from Blizzard/heroprotocol...",
116
+ );
117
+ const files = (await fetchJson(PROTOCOLS_LIST_URL)) as GitHubFile[];
118
+
119
+ if (!Array.isArray(files)) {
120
+ console.error(
121
+ "@astefanski/storm-parser: Failed to fetch protocol list:",
122
+ files,
123
+ );
124
+ process.exit(1);
125
+ }
126
+
127
+ const protocols = files
128
+ .filter((f) => PROTOCOL_REGEX.test(f.name))
129
+ .map((f) => f.name.match(PROTOCOL_REGEX)![1]);
130
+
131
+ console.log(
132
+ `@astefanski/storm-parser: Found ${protocols.length} protocols. Downloading...`,
133
+ );
134
+
135
+ let completed = 0;
136
+ // Process in batches of 10 to avoid rate limiting
137
+ const BATCH_SIZE = 10;
138
+ for (let i = 0; i < protocols.length; i += BATCH_SIZE) {
139
+ const batch = protocols.slice(i, i + BATCH_SIZE);
140
+ await Promise.all(
141
+ batch.map(async (proto) => {
142
+ const pyFile = `protocol${proto}.py`;
143
+ const code = await fetchText(RAW_BASE_URL + pyFile);
144
+
145
+ const protocolData = {
146
+ version: parseInt(proto, 10),
147
+ typeinfos: extractVariable(code, "typeinfos"),
148
+ game_event_types: extractVariable(code, "game_event_types"),
149
+ game_eventid_typeid: extractVariable(code, "game_eventid_typeid"),
150
+ message_event_types: extractVariable(code, "message_event_types"),
151
+ message_eventid_typeid: extractVariable(
152
+ code,
153
+ "message_eventid_typeid",
154
+ ),
155
+ tracker_event_types: extractVariable(code, "tracker_event_types"),
156
+ tracker_eventid_typeid: extractVariable(
157
+ code,
158
+ "tracker_eventid_typeid",
159
+ ),
160
+ svaruint32_typeid: extractVariable(code, "svaruint32_typeid"),
161
+ replay_userid_typeid: extractVariable(code, "replay_userid_typeid"),
162
+ replay_header_typeid: extractVariable(code, "replay_header_typeid"),
163
+ game_details_typeid: extractVariable(code, "game_details_typeid"),
164
+ replay_initdata_typeid: extractVariable(
165
+ code,
166
+ "replay_initdata_typeid",
167
+ ),
168
+ };
169
+
170
+ fs.writeFileSync(
171
+ path.join(outDir, `protocol${proto}.json`),
172
+ JSON.stringify(protocolData),
173
+ );
174
+
175
+ completed++;
176
+ if (completed % 50 === 0 || completed === protocols.length) {
177
+ console.log(
178
+ `@astefanski/storm-parser: Downloaded ${completed}/${protocols.length} protocols`,
179
+ );
180
+ }
181
+ }),
182
+ );
183
+ }
184
+
185
+ console.log("@astefanski/storm-parser: All protocols downloaded!");
186
+ }
187
+
188
+ main().catch((err) => {
189
+ console.error("@astefanski/storm-parser: Postinstall failed:", err);
190
+ process.exit(1);
191
+ });