Repository: PBug90/w3gjs
Pull request #199: Parsing touchups
Author: login: BogdanW3
Created: 2025-04-19T09:39:16Z | Merged: 2025-04-20T15:43:40Z



Diff:
diff --git a/src/Player.ts b/src/Player.ts
index 1db032a6..802d626d 100644
--- a/src/Player.ts
+++ b/src/Player.ts
@@ -339,7 +339,6 @@ class Player {
 
   determineHeroLevelsAndHandleRetrainings() {
     const heroInfo: Omit<HeroInfo, "order">[] = [];
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
     for (const { order, ...hero } of Object.values(this.heroCollector).sort(
       (h1, h2) => h1.order - h2.order,
     )) {
diff --git a/src/W3GReplay.ts b/src/W3GReplay.ts
index 727a52a0..d539051b 100644
--- a/src/W3GReplay.ts
+++ b/src/W3GReplay.ts
@@ -285,25 +285,25 @@ export default class W3GReplay extends EventEmitter implements W3GReplayEvents {
     switch (action.id) {
       case 0x10:
         if (
-          objectIdFormatter(action.itemId).value === "tert" ||
-          objectIdFormatter(action.itemId).value === "tret"
+          objectIdFormatter(action.orderId).value === "tert" ||
+          objectIdFormatter(action.orderId).value === "tret"
         ) {
           currentPlayer.handleRetraining(this.totalTimeTracker);
         }
         currentPlayer.handle0x10(
-          objectIdFormatter(action.itemId),
+          objectIdFormatter(action.orderId),
           this.totalTimeTracker,
         );
         break;
       case 0x11:
         currentPlayer.handle0x11(
-          objectIdFormatter(action.itemId),
+          objectIdFormatter(action.orderId),
           this.totalTimeTracker,
         );
         break;
       case 0x12:
         currentPlayer.handle0x12(
-          objectIdFormatter(action.itemId),
+          objectIdFormatter(action.orderId),
           this.totalTimeTracker,
         );
         break;
@@ -311,7 +311,7 @@ export default class W3GReplay extends EventEmitter implements W3GReplayEvents {
         currentPlayer.handle0x13();
         break;
       case 0x14:
-        currentPlayer.handle0x14(objectIdFormatter(action.itemId1));
+        currentPlayer.handle0x14(objectIdFormatter(action.orderId1));
         break;
       case 0x16:
         if (action.selectMode === 0x02) {
diff --git a/src/index.ts b/src/index.ts
index cfcc1ffe..716f50d6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,16 @@
 import W3GReplay from "./W3GReplay";
+
+import GameDataParser from "./parsers/GameDataParser";
+import MetadataParser from "./parsers/MetadataParser";
+import RawParser, { DataBlock, getUncompressedData } from "./parsers/RawParser";
 import ReplayParser from "./parsers/ReplayParser";
+
 export default W3GReplay;
-export { ReplayParser };
+export {
+  GameDataParser,
+  MetadataParser,
+  RawParser,
+  ReplayParser,
+  getUncompressedData,
+  DataBlock,
+};
diff --git a/src/parsers/ActionParser.ts b/src/parsers/ActionParser.ts
index 43092366..20ed7de4 100644
--- a/src/parsers/ActionParser.ts
+++ b/src/parsers/ActionParser.ts
@@ -1,71 +1,75 @@
 import StatefulBufferParser from "./StatefulBufferParser";
 
+type NetTag = [number, number];
+type Vec2 = [number, number];
+
 type UnitBuildingAbilityActionNoParams = {
   id: 0x10;
   abilityFlags: number;
-  itemId: number[];
+  orderId: number[];
 };
 
 type UnitBuildingAbilityActionTargetPosition = {
   id: 0x11;
   abilityFlags: number;
-  itemId: number[];
-  targetX: number;
-  targetY: number;
+  orderId: number[];
+  target: Vec2;
 };
 
 type UnitBuildingAbilityActionTargetPositionTargetObjectId = {
   id: 0x12;
   abilityFlags: number;
-  itemId: number[];
-  targetX: number;
-  targetY: number;
-  objectId1: number;
-  objectId2: number;
-};
-
-export type TransferResourcesAction = {
-  id: 0x51;
-  slot: number;
-  gold: number;
-  lumber: number;
+  orderId: number[];
+  target: Vec2;
+  object: NetTag;
 };
 
 type GiveItemToUnitAciton = {
   id: 0x13;
   abilityFlags: number;
-  itemId: number[];
-  targetX: number;
-  targetY: number;
-  objectId1: number;
-  objectId2: number;
-  itemObjectId1: number;
-  itemObjectId2: number;
+  orderId: number[];
+  target: Vec2;
+  unit: NetTag;
+  item: NetTag;
 };
 
 type UnitBuildingAbilityActionTwoTargetPositions = {
   id: 0x14;
   abilityFlags: number;
-  itemId1: number[];
-  targetAX: number;
-  targetAY: number;
-  itemId2: number[];
-  targetBX: number;
-  targetBY: number;
+  orderId1: number[];
+  targetA: Vec2;
+  orderId2: number[];
+  flags: number;
+  category: number;
+  owner: number;
+  targetB: Vec2;
+};
+
+type UnitBuildingAbilityActionTargetPositionTargetObjectIdItemObjectId = {
+  id: 0x15;
+  abilityFlags: number;
+  orderId1: number[];
+  targetA: Vec2;
+  orderId2: number[];
+  flags: number;
+  category: number;
+  owner: number;
+  targetB: Vec2;
+  object: NetTag;
 };
 
 type ChangeSelectionAction = {
   id: 0x16;
   selectMode: number;
   numberUnits: number;
-  actions: { itemId1: number[]; itemId2: number[] }[];
+  units: NetTag[];
 };
 
 type AssignGroupHotkeyAction = {
   id: 0x17;
   groupNumber: number;
   numberUnits: number;
-  actions: { itemId1: number[]; itemId2: number[] }[];
+  units: NetTag[];
 };
 
 type SelectGroupHotkeyAction = {
@@ -73,35 +77,29 @@ type SelectGroupHotkeyAction = {
   groupNumber: number;
 };
 
-type SelectGroundItemAction = {
-  id: 0x1c;
-  itemId1: number[];
-  itemId2: number[];
-};
-
 type SelectSubgroupAction = {
   id: 0x19;
   itemId: number[];
-  objectId1: number;
-  objectId2: number;
+  object: NetTag;
 };
 
-type CancelHeroRevival = {
-  id: 0x1d;
-  itemId1: number[];
-  itemId2: number[];
+type PreSubselectionAction = {
+  id: 0x1a;
 };
 
-type ChooseHeroSkillSubmenu = {
-  id: 0x65 | 0x66;
+type SelectUnitAction = {
+  id: 0x1b;
+  object: NetTag;
 };
 
-type EnterBuildingSubmenu = {
-  id: 0x67;
+type SelectGroundItemAction = {
+  id: 0x1c;
+  item: NetTag;
 };
 
-type ESCPressedAction = {
-  id: 0x61;
+type CancelHeroRevival = {
+  id: 0x1d;
+  hero: NetTag;
 };
 
 type RemoveUnitFromBuildingQueue = {
@@ -110,27 +108,169 @@ type RemoveUnitFromBuildingQueue = {
   itemId: number[];
 };
 
-type PreSubselectionAction = {
-  id: 0x1a;
+export type TransferResourcesAction = {
+  id: 0x51;
+  slot: number;
+  gold: number;
+  lumber: number;
 };
 
-export type W3MMDAction = {
-  id: 0x6b;
+type ESCPressedAction = {
+  id: 0x61;
+};
+
+type ChooseHeroSkillSubmenu = {
+  id: 0x66;
+};
+
+type EnterBuildingSubmenu = {
+  id: 0x67;
+};
+
+type Cache = {
   filename: string;
   missionKey: string;
   key: string;
+};
+
+type BlzCacheStoreIntAction = {
+  id: 0x6b;
+  cache: Cache;
+  value: number;
+};
+
+export type W3MMDAction = BlzCacheStoreIntAction;
+
+type BlzCacheStoreRealAction = {
+  id: 0x6c;
+  cache: Cache;
   value: number;
 };
 
+type BlzCacheStoreBooleanAction = {
+  id: 0x6d;
+  cache: Cache;
+  value: number;
+};
+
+export type Item = {
+  itemId: number[];
+  charges: number;
+  flags: number;
+};
+
+export type Ability = {
+  id: number[];
+  level: number;
+};
+
+type HeroData = {
+  xp: number;
+  level: number;
+  skillPoints: number;
+  properNameId: number;
+  str: number;
+  strBonus: number; //float
+  agi: number;
+  speedMod: number; //float
+  cooldownMod: number; //float
+  agiBonus: number; //float
+  intel: number;
+  intBonus: number; //float
+  heroAbils: Ability[];
+  maxLife: number; //float
+  maxMana: number; //float
+  // >= 6030
+  sight: number; //float
+  damage: number[]; //int[]
+  defense: number; //float
+  // >= 6031
+  controlGroups: number; //int16
+};
+
+export type Unit = {
+  unitId: number[];
+  items: Item[];
+  heroData: HeroData;
+};
+
+type BlzCacheStoreUnitAction = {
+  id: 0x6e;
+  cache: Cache;
+  value: Unit;
+};
+
+type BlzCacheClearIntAction = {
+  id: 0x70;
+  cache: Cache;
+};
+
+type BlzCacheClearRealAction = {
+  id: 0x71;
+  cache: Cache;
+};
+
+type BlzCacheClearBooleanAction = {
+  id: 0x72;
+  cache: Cache;
+};
+
+type BlzCacheClearUnitAction = {
+  id: 0x73;
+  cache: Cache;
+};
+
+type ArrowKeyAction = {
+  id: 0x75;
+  arrowKey: number;
+};
+
 type BlzSyncAction = {
   id: 0x78;
   identifier: string;
   value: string;
 };
 
-type UnknownAction = {
-  id: 0x79 | 0x76 | 0x73;
-  action: Buffer;
+type CommandFrameAction = {
+  id: 0x79;
+  eventId: number;
+  val: number;
+  text: string;
+};
+
+type MouseAction = {
+  id: 0x76;
+  eventId: number;
+  pos: Vec2;
+  button: number;
+};
+
+type W3APIAction = {
+  id: 0x77;
+  commandId: number;
+  data: number;
+  buffer: string;
+};
+
+type SetGameSpeedAction = {
+  id: 0x3;
+  gameSpeed: number;
+};
+
+type TrackableHitAction = {
+  id: 0x64;
+  object: NetTag;
+};
+
+type TrackableTrackAction = {
+  id: 0x65;
+  object: NetTag;
+};
+
+type AllyPingAction = {
+  id: 0x68;
+  pos: Vec2;
+  duration: number;
 };
 
 export type Action =
@@ -146,14 +286,30 @@ export type Action =
   | SelectGroundItemAction
   | CancelHeroRevival
   | RemoveUnitFromBuildingQueue
-  | W3MMDAction
+  | BlzCacheStoreIntAction
+  | BlzCacheStoreRealAction
+  | BlzCacheStoreBooleanAction
+  | BlzCacheStoreUnitAction
+  | BlzCacheClearIntAction
+  | BlzCacheClearRealAction
+  | BlzCacheClearBooleanAction
+  | BlzCacheClearUnitAction
   | ESCPressedAction
   | ChooseHeroSkillSubmenu
   | EnterBuildingSubmenu
   | TransferResourcesAction
   | UnitBuildingAbilityActionTargetPosition
   | BlzSyncAction
-  | UnknownAction;
+  | CommandFrameAction
+  | MouseAction
+  | W3APIAction
+  | SetGameSpeedAction
+  | UnitBuildingAbilityActionTargetPositionTargetObjectIdItemObjectId
+  | SelectUnitAction
+  | TrackableHitAction
+  | TrackableTrackAction
+  | AllyPingAction
+  | ArrowKeyAction;
 
 export default class ActionParser extends StatefulBufferParser {
   parse(input: Buffer): Action[] {
@@ -172,351 +328,460 @@ export default class ActionParser extends StatefulBufferParser {
     return actions;
   }
 
-  private parseAction(actionId: number): Action | null {
-    switch (actionId) {
-      case 0x79: {
-        const action = {
-          id: 0x79,
-          action: this.buffer.slice(
-            this.getOffset() - 1,
-            this.getOffset() + 0x11,
-          ),
-        } as const;
-        this.skip(0x11);
-        return action;
-      }
-      case 0x78:
-        const identifier = this.readZeroTermString("utf8");
-        const value = this.readZeroTermString("utf8");
-        this.skip(4);
-        return {
-          id: actionId,
-          value,
-          identifier,
-        };
-
-      case 0x76: {
-        const action = {
-          id: 0x73,
-          action: this.buffer.slice(
-            this.getOffset() - 1,
-            this.getOffset() + 0xa,
-          ),
-        } as const;
-        this.skip(0xa);
-        return action;
-      }
-      case 0x73: {
-        const action = {
-          id: 0x73,
-          action: this.buffer.slice(
-            this.getOffset() - 1,
-            this.getOffset() + 0x6,
-          ),
-        } as const;
-        this.skip(0x6);
-        return action;
-      }
-      case 0x1:
-        break;
-      case 0x2:
-        break;
-      case 0x3:
-        this.skip(1);
-        break;
-      case 0x4:
-      case 0x5:
-        break;
-      case 0x6:
-        this.readZeroTermString("utf-8");
-        break;
-      case 0x7:
-        this.skip(2);
-        break;
-      case 0x10: {
-        const abilityFlags = this.readUInt16LE();
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.readUInt32LE(); // unknownA
-        this.readUInt32LE(); // unknownB
-        return { id: actionId, abilityFlags, itemId };
-      }
-      case 0x11: {
-        const abilityFlags = this.readUInt16LE();
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.readUInt32LE(); // unknownA
-        this.readUInt32LE(); // unknownB
-        const targetX = this.readFloatLE();
-        const targetY = this.readFloatLE();
-        return { id: actionId, abilityFlags, itemId, targetX, targetY };
-      }
-
-      case 0x12: {
-        const abilityFlags = this.readUInt16LE();
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.readUInt32LE(); // unknownA
-        this.readUInt32LE(); // unknownB
-        const targetX = this.readFloatLE();
-        const targetY = this.readFloatLE();
-        const objectId1 = this.readUInt32LE();
-        const objectId2 = this.readUInt32LE();
-        return {
-          id: actionId,
-          abilityFlags,
-          itemId,
-          targetX,
-          targetY,
-          objectId1,
-          objectId2,
-        };
+  private writeHexString(data: string) {
+    for (let i = 0; i < data.length; ) {
+      process.stdout.write(data[i] + data[i + 1]);
+      i += 2;
+      process.stdout.write(" ");
+      if (i % 32 == 0 && i < data.length) {
+        process.stdout.write("\n");
       }
+    }
+    console.log();
+  }
 
-      case 0x13: {
-        const abilityFlags = this.readUInt16LE();
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.readUInt32LE(); // unknownA
-        this.readUInt32LE(); // unknownB
-        const targetX = this.readFloatLE();
-        const targetY = this.readFloatLE();
-        const objectId1 = this.readUInt32LE();
-        const objectId2 = this.readUInt32LE();
-        const itemObjectId1 = this.readUInt32LE();
-        const itemObjectId2 = this.readUInt32LE();
-        return {
-          id: actionId,
-          abilityFlags,
-          itemId,
-          targetX,
-          targetY,
-          objectId1,
-          objectId2,
-          itemObjectId1,
-          itemObjectId2,
-        };
-      }
-      case 0x14: {
-        const abilityFlags = this.readUInt16LE();
-        const itemId1 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.readUInt32LE(); // unknownA
-        this.readUInt32LE(); // unknownB
-        const targetAX = this.readFloatLE();
-        const targetAY = this.readFloatLE();
-        const itemId2 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        this.skip(9);
-        const targetBX = this.readFloatLE();
-        const targetBY = this.readFloatLE();
-        return {
-          id: actionId,
-          abilityFlags,
-          itemId1,
-          targetAX,
-          targetAY,
-          itemId2,
-          targetBX,
-          targetBY,
-        };
-      }
-      case 0x16: {
-        const selectMode = this.readUInt8();
-        const numberUnits = this.readUInt16LE();
-        const actions = this.readSelectionUnits(numberUnits);
-        return { id: actionId, selectMode, numberUnits, actions };
-      }
-      case 0x17: {
-        const groupNumber = this.readUInt8();
-        const numberUnits = this.readUInt16LE();
-        const actions = this.readSelectionUnits(numberUnits);
-        return { id: actionId, groupNumber, numberUnits, actions };
-      }
-      case 0x18: {
-        const groupNumber = this.readUInt8();
-        this.skip(1);
-        return { id: actionId, groupNumber };
-      }
-      case 0x19: {
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        const objectId1 = this.readUInt32LE();
-        const objectId2 = this.readUInt32LE();
-        return { id: actionId, itemId, objectId1, objectId2 };
-      }
-      case 0x1a: {
-        return { id: actionId };
-      }
+  private oldActionId: number = 999999;
+  private parseAction(actionId: number): Action | null {
+    try {
+      switch (actionId) {
+        // no action 0x00
+        case 0x1:
+          this.skip(1);
+          break;
+        case 0x2:
+          break;
+        case 0x3:
+          const gameSpeed = this.readUInt8();
+          return { id: actionId, gameSpeed };
+        case 0x4:
+        case 0x5:
+          break;
+        case 0x6:
+          this.readZeroTermString("utf-8");
+          this.readZeroTermString("utf-8");
+          this.readUInt8();
+          break;
+        case 0x7:
+          this.skip(4);
+          break;
+        // no actions 0x08 - 0x0f
+        case 0x10: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId = this.readFourCC();
+          this.skip(8); // AgentTag
+          return { id: actionId, abilityFlags, orderId };
+        }
+        case 0x11: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId = this.readFourCC();
+          this.skip(8); // AgentTag
+          const target = this.readVec2();
+          return { id: actionId, abilityFlags, orderId, target };
+        }
+        case 0x12: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId = this.readFourCC();
+          this.skip(8); // AgentTag
+          const target = this.readVec2();
+          const object = this.readNetTag();
+          return {
+            id: actionId,
+            abilityFlags,
+            orderId,
+            target,
+            object,
+          };
+        }
+        case 0x13: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId = this.readFourCC();
+          this.skip(8); // AgentTag
+          const target = this.readVec2();
+          const unit = this.readNetTag();
+          const item = this.readNetTag();
+          return {
+            id: actionId,
+            abilityFlags,
+            orderId,
+            target,
+            unit,
+            item,
+          };
+        }
+        case 0x14: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId1 = this.readFourCC();
+          this.skip(8); // AgentTag
+          const targetA = this.readVec2();
+          const orderId2 = this.readFourCC();
+          const flags = this.readUInt32LE();
+          const category = this.readUInt32LE();
+          const owner = this.readUInt8();
+          const targetB = this.readVec2();
+          return {
+            id: actionId,
+            abilityFlags,
+            orderId1,
+            targetA,
+            orderId2,
+            flags,
+            category,
+            owner,
+            targetB,
+          };
+        }
+        case 0x15: {
+          const abilityFlags = this.readUInt16LE();
+          const orderId1 = this.readFourCC();
+          this.skip(8); // AgentTag
+          const targetA = this.readVec2();
+          const orderId2 = this.readFourCC();
+          const flags = this.readUInt32LE();
+          const category = this.readUInt32LE();
+          const owner = this.readUInt8();
+          const targetB = this.readVec2();
+          const object = this.readNetTag();
+          return {
+            id: actionId,
+            abilityFlags,
+            orderId1,
+            targetA,
+            orderId2,
+            flags,
+            category,
+            owner,
+            targetB,
+            object,
+          };
+        }
+        case 0x16: {
+          const selectMode = this.readUInt8();
+          const numberUnits = this.readUInt16LE();
+          const actions = this.readSelectionUnits(numberUnits);
+          return { id: actionId, selectMode, numberUnits, units: actions };
+        }
+        case 0x17: {
+          const groupNumber = this.readUInt8();
+          const numberUnits = this.readUInt16LE();
+          const actions = this.readSelectionUnits(numberUnits);
+          return { id: actionId, groupNumber, numberUnits, units: actions };
+        }
+        case 0x18: {
+          const groupNumber = this.readUInt8();
+          this.skip(1);
+          return { id: actionId, groupNumber };
+        }
+        case 0x19: {
+          const itemId = this.readFourCC();
+          const object = this.readNetTag();
+          return { id: actionId, itemId, object };
+        }
+        case 0x1a: {
+          return { id: actionId };
+        }
+        case 0x1b: {
+          this.skip(1);
+          const object = this.readNetTag();
+          return { id: actionId, object };
+        }
+        case 0x1c: {
+          this.skip(1);
+          const item = this.readNetTag();
+          return { id: actionId, item };
+        }
+        case 0x1d: {
+          const hero = this.readNetTag();
+          return { id: actionId, hero };
+        }
+        case 0x1e:
+        // couldn't find action 0x1f in reference
+        case 0x1f: {
+          const slotNumber = this.readUInt8();
+          const itemId = this.readFourCC();
+          return { id: actionId, slotNumber, itemId };
+        }
+        // 0x20 to 0x4f are cheat actions
+        case 0x20:
+          break;
+        case 0x21:
+          this.skip(8);
+          break;
+        case 0x22:
+        case 0x23:
+        case 0x24:
+        case 0x25:
+        case 0x26:
+          break;
+        case 0x27:
+        case 0x28:
+          this.skip(5);
+          break;
+        case 0x29:
+        case 0x2a:
+        case 0x2b:
+        case 0x2c:
+          break;
+        case 0x2d:
+          this.skip(5);
+          break;
+        case 0x2e:
+          this.skip(4);
+          break;
+        case 0x2f:
+          break;
+        // TODO: the rest of the cheats
 
-      case 0x1b: {
-        this.skip(9);
-        return null;
-      }
-      case 0x1c: {
-        this.skip(1);
-        const itemId1 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        const itemId2 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        return { id: actionId, itemId1, itemId2 };
+        // END OF TODO
+        case 0x50:
+          this.readUInt8(); // slotNumber
+          this.readUInt32LE(); // flags
+          return null;
+        case 0x51:
+          const slot = this.readUInt8();
+          const gold = this.readUInt32LE();
+          const lumber = this.readUInt32LE();
+          return { id: actionId, slot, gold, lumber };
+        // no actions 0x52 - 0x5f
+        case 0x60:
+          this.skip(8);
+          this.readZeroTermString("utf-8");
+          return null;
+        case 0x61:
+          return { id: actionId };
+        case 0x62:
+          this.skip(12);
+          return null;
+        case 0x63:
+          this.skip(8);
+          return null;
+        case 0x64:
+        case 0x65: {
+          const object = this.readNetTag();
+          return { id: actionId, object };
+        }
+        case 0x66:
+        case 0x67:
+          return {
+            id: actionId,
+          };
+        case 0x68: {
+          const pos = this.readVec2();
+          const duration = this.readFloatLE();
+          return { id: actionId, pos, duration };
+        }
+        case 0x69:
+        case 0x6a: {
+          this.skip(16);
+          break;
+        }
+        case 0x6b: {
+          const cache = this.readCacheDesc();
+          const value = this.readUInt32LE();
+          return { id: actionId, cache, value };
+        }
+        case 0x6c: {
+          const cache = this.readCacheDesc();
+          const value = this.readFloatLE();
+          return { id: actionId, cache, value };
+        }
+        case 0x6d: {
+          const cache = this.readCacheDesc();
+          const value = this.readUInt8();
+          return { id: actionId, cache, value };
+        }
+        case 0x6e: {
+          const cache = this.readCacheDesc();
+          const value = this.readCacheUnit();
+          return { id: actionId, cache, value };
+        }
+        // no action 0x6f
+        case 0x70: {
+          const cache = this.readCacheDesc();
+          return { id: actionId, cache };
+        }
+        case 0x71: {
+          const cache = this.readCacheDesc();
+          return { id: actionId, cache };
+        }
+        case 0x72: {
+          const cache = this.readCacheDesc();
+          return { id: actionId, cache };
+        }
+        case 0x73: {
+          const cache = this.readCacheDesc();
+          return { id: actionId, cache };
+        }
+        // no action 0x74
+        case 0x75: {
+          const arrowKey = this.readUInt8();
+          return { id: actionId, arrowKey };
+          break;
+        }
+        case 0x76: {
+          const eventId = this.readUInt8();
+          const pos = this.readVec2();
+          const button = this.readUInt8();
+          return { id: actionId, eventId, pos, button };
+        }
+        case 0x77: {
+          const commandId = this.readUInt32LE();
+          const data = this.readUInt32LE();
+          const buffLen = this.readUInt32LE();
+          const buffer = this.readStringOfLength(buffLen, "utf-8");
+          return { id: actionId, commandId, data, buffer };
+        }
+        case 0x78: {
+          const identifier = this.readZeroTermString("utf8");
+          const value = this.readZeroTermString("utf8");
+          this.skip(4);
+          return { id: actionId, identifier, value };
+        }
+        case 0x79: {
+          this.skip(8);
+          const eventId = this.readUInt32LE();
+          const val = this.readFloatLE();
+          const text = this.readZeroTermString("utf-8");
+          return { id: actionId, eventId, val, text };
+        }
+        case 0x7a:
+          this.skip(20);
+          break;
+        case 0x7b:
+          this.skip(16);
+          break;
+        // no actions 0x7c - 0x80
+        // 0x81 - 0x85 are replay actions
+        // no actions 0x86 - 0x9f
+        // 0xa0 and 0xa1 are cheats
+        case 0xa0:
+          this.skip(14);
+          break;
+        case 0xa1:
+          this.skip(9);
+        default:
+          console.log(
+            "unknown action id ",
+            actionId,
+            " after ",
+            this.oldActionId,
+          );
+          return null;
       }
-      case 0x1d: {
-        const itemId1 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        const itemId2 = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        return { id: actionId, itemId1, itemId2 };
-      }
-      case 0x1e:
-      case 0x1f: {
-        const slotNumber = this.readUInt8();
-        const itemId = [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ];
-        return { id: actionId, slotNumber, itemId };
-      }
-      case 0x27:
-      case 0x28:
-      case 0x2d:
-        this.skip(5);
-        break;
-      case 0x2e:
-        this.skip(4);
-        break;
-      case 0x50:
-        this.readUInt8(); // slotNumber
-        this.readUInt32LE(); // flags
-        return null;
-      case 0x51:
-        const slot = this.readUInt8();
-        const gold = this.readUInt32LE();
-        const lumber = this.readUInt32LE();
-        return {
-          id: 0x51,
-          slot: slot,
-          gold: gold,
-          lumber,
-        };
-      case 0x60:
-        this.skip(8);
-        this.readZeroTermString("utf-8");
-        return null;
-      case 0x61:
-        return { id: 0x61 };
-      case 0x62:
-        this.skip(12);
-        return null;
-      case 0x65:
-      case 0x66:
-      case 0x67:
-        return {
-          id: actionId,
-        };
-      case 0x68: {
-        this.skip(12);
-        return null;
-      }
-      case 0x69:
-      case 0x6a: {
-        this.skip(16);
-        return null;
-      }
-      case 0x6b: {
-        const filename = this.readZeroTermString("utf-8");
-        const missionKey = this.readZeroTermString("utf-8");
-        const key = this.readZeroTermString("utf-8");
-        const value = this.readUInt32LE();
-        return { id: actionId, filename, missionKey, key, value };
-      }
-      case 0x75: {
-        this.skip(1);
-        return null;
-      }
-      case 0x77:
-        this.skip(13);
-        return null;
-      case 0x7a:
-        this.skip(20);
-        return null;
-      case 0x7b:
-        this.skip(16);
-        return null;
-      default:
-        console.log("unknown action id ", actionId);
-        return null;
+      return null;
+    } finally {
+      this.oldActionId = actionId;
     }
-    return null;
   }
 
-  private readSelectionUnits(
-    length: number,
-  ): { itemId1: number[]; itemId2: number[] }[] {
-    const v: { itemId1: number[]; itemId2: number[] }[] = [];
+  private readSelectionUnits(length: number): NetTag[] {
+    const v: NetTag[] = [];
     for (let i = 0; i < length; i++) {
-      const obj = {
-        itemId1: [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ],
-        itemId2: [
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-          this.readUInt8(),
-        ],
-      };
-      v.push(obj);
+      v.push(this.readNetTag());
     }
     return v;
   }
+
+  private readFourCC(): number[] {
+    const fourCC = [
+      this.readUInt8(),
+      this.readUInt8(),
+      this.readUInt8(),
+      this.readUInt8(),
+    ];
+    return fourCC;
+  }
+
+  private readCacheDesc(): Cache {
+    const filename = this.readZeroTermString("utf-8");
+    const missionKey = this.readZeroTermString("utf-8");
+    const key = this.readZeroTermString("utf-8");
+    return { filename, missionKey, key };
+  }
+
+  private readCacheItem(): Item {
+    const itemId = this.readFourCC();
+    const charges = this.readUInt32LE();
+    const flags = this.readUInt32LE();
+    return { itemId, charges, flags };
+  }
+
+  private readAbility(): Ability {
+    const id = this.readFourCC();
+    const level = this.readUInt32LE();
+    return { id, level };
+  }
+
+  private readCacheHeroData(): HeroData {
+    const xp = this.readUInt32LE();
+    const level = this.readUInt32LE();
+    const skillPoints = this.readUInt32LE();
+    const properNameId = this.readUInt32LE();
+    const str = this.readUInt32LE();
+    const strBonus = this.readFloatLE();
+    const agi = this.readUInt32LE();
+    const speedMod = this.readFloatLE();
+    const cooldownMod = this.readFloatLE();
+    const agiBonus = this.readFloatLE();
+    const intel = this.readUInt32LE();
+    const intBonus = this.readFloatLE();
+    const heroAbilCount = this.readUInt32LE();
+    const heroAbils: Ability[] = [];
+    for (let i = 0; i < heroAbilCount; i++) {
+      heroAbils.push(this.readAbility());
+    }
+    const maxLife = this.readFloatLE();
+    const maxMana = this.readFloatLE();
+    const sight = this.readFloatLE();
+    const damageCount = this.readUInt32LE();
+    const damage: number[] = [];
+    for (let i = 0; i < damageCount; i++) {
+      damage.push(this.readUInt32LE());
+    }
+    const defense = this.readFloatLE();
+    const controlGroups = this.readUInt16LE();
+    return {
+      xp,
+      level,
+      skillPoints,
+      properNameId,
+      str,
+      strBonus,
+      agi,
+      speedMod,
+      cooldownMod,
+      agiBonus,
+      intel,
+      intBonus,
+      heroAbils,
+      maxLife,
+      maxMana,
+      sight,
+      damage,
+      defense,
+      controlGroups,
+    };
+  }
+
+  private readCacheUnit(): Unit {
+    const unitId = this.readFourCC();
+    const itemsCount = this.readUInt32LE();
+    const items: Item[] = [];
+    for (let i = 0; i < itemsCount; i++) {
+      items.push(this.readCacheItem());
+    }
+    const heroData = this.readCacheHeroData();
+    return { unitId, items, heroData };
+  }
+
+  private readNetTag(): NetTag {
+    const tag1 = this.readUInt32LE();
+    const tag2 = this.readUInt32LE();
+    return [tag1, tag2];
+  }
+
+  private readVec2(): Vec2 {
+    const x = this.readFloatLE();
+    const y = this.readFloatLE();
+    return [x, y];
+  }
 }
diff --git a/src/parsers/MetadataParser.ts b/src/parsers/MetadataParser.ts
index c225e254..b94143f2 100644
--- a/src/parsers/MetadataParser.ts
+++ b/src/parsers/MetadataParser.ts
@@ -1,5 +1,4 @@
-import { inflate, constants } from "zlib";
-import { DataBlock } from "./RawParser";
+import { DataBlock, getUncompressedData } from "./RawParser";
 import StatefulBufferParser from "./StatefulBufferParser";
 import { Type, Field } from "protobufjs";
 
@@ -11,23 +10,33 @@ const protoPlayer = new Type("ReforgedPlayerData")
   .add(new Field("team", 5, "uint32"))
   .add(new Field("unknown", 6, "string"));
 
-const inflatePromise = (buffer: Buffer, options = {}): Promise<Buffer> =>
-  new Promise((resolve, reject) => {
-    inflate(buffer, options, (err, result) => {
-      if (err !== null) {
-        reject(err);
-      }
-      resolve(result);
-    });
-  });
+type SkinData = {
+  unitId: number;
+  skinId: number;
+  skinName: string;
+};
+
+const protoSkinData = new Type("SkinData")
+  .add(new Field("unitId", 1, "uint32"))
+  .add(new Field("skinId", 2, "uint32"))
+  .add(new Field("skinName", 3, "string"));
+
+const protoSkin = new Type("ReforgedSkinData")
+  .add(protoSkinData)
+  .add(new Field("playerId", 1, "uint32"))
+  .add(new Field("skins", 2, "SkinData", "repeated"));
 
 export type ReplayMetadata = {
   gameData: Buffer;
   map: MapMetadata;
+  playerCount: number;
+  gameType: string;
+  localeHash: string;
   playerRecords: PlayerRecord[];
   slotRecords: SlotRecord[];
   reforgedPlayerMetadata: ReforgedPlayerMetadata[];
   randomSeed: number;
+  selectMode: string;
   gameName: string;
   startSpotCount: number;
 };
@@ -39,6 +48,8 @@ type PlayerRecord = {
 
 type SlotRecord = {
   playerId: number;
+  // 0-100, -1 for bots and embedded maps
+  downloadProgress: number;
   slotStatus: number;
   computerFlag: number;
   teamId: number;
@@ -52,6 +63,7 @@ type ReforgedPlayerMetadata = {
   playerId: number;
   name: string;
   clan: string;
+  skins: SkinData[];
 };
 
 type MapMetadata = {
@@ -75,17 +87,13 @@ type MapMetadata = {
 
 export default class MetadataParser extends StatefulBufferParser {
   private mapmetaParser: StatefulBufferParser = new StatefulBufferParser();
+
   async parse(blocks: DataBlock[]): Promise<ReplayMetadata> {
-    const buffs: Buffer[] = [];
-    for (const block of blocks) {
-      const block2 = await inflatePromise(block.blockContent, {
-        finishFlush: constants.Z_SYNC_FLUSH,
-      });
-      if (block2.byteLength > 0 && block.blockContent.byteLength > 0) {
-        buffs.push(block2);
-      }
-    }
-    this.initialize(Buffer.concat(buffs));
+    return this.parseData(await getUncompressedData(blocks));
+  }
+
+  public async parseData(data: Buffer): Promise<ReplayMetadata> {
+    this.initialize(data);
     this.skip(5);
     const playerRecords = [];
     playerRecords.push(this.parseHostRecord());
@@ -95,29 +103,46 @@ export default class MetadataParser extends StatefulBufferParser {
     const mapMetadata = this.parseEncodedMapMetaString(
       this.decodeGameMetaString(encodedString),
     );
-    this.skip(12);
+    const playerCount = this.readUInt32LE();
+    const gameType = this.readStringOfLength(4, "hex");
+    const localeHash = this.readStringOfLength(4, "hex");
     const playerListFinal = playerRecords.concat(
       playerRecords,
       this.parsePlayerList(),
     );
     let reforgedPlayerMetadata: ReforgedPlayerMetadata[] = [];
-    if (this.readUInt8() !== 25) {
-      this.skip(-1);
+    if (this.peekUInt8() !== 25) {
       reforgedPlayerMetadata = this.parseReforgedPlayerMetadata();
     }
-    this.skip(2);
+    if (this.readUInt8() !== 25) {
+      console.log(
+        "Unknown chunk detected!",
+        this.buffer.subarray(this.getOffset() - 1),
+      );
+    }
+    const remainingBytes = this.readUInt16LE();
     const slotRecordCount = this.readUInt8();
+    // remaining bytes are: slotRecordCount(1), slots(9*count), seed(4), mode(1), spots(1)
+    if (remainingBytes !== 1 + slotRecordCount * 9 + 6) {
+      console.log(
+        `Remaining bytes (${remainingBytes}) do not match expected bytes (${1 + slotRecordCount * 9 + 6})`,
+      );
+    }
     const slotRecords = this.parseSlotRecords(slotRecordCount);
     const randomSeed = this.readUInt32LE();
-    this.skip(1);
+    const selectMode = this.readStringOfLength(1, "hex");
     const startSpotCount = this.readUInt8();
     return {
       gameData: this.buffer.subarray(this.getOffset()),
       map: mapMetadata,
+      playerCount,
+      gameType,
+      localeHash,
       playerRecords: playerListFinal,
       slotRecords,
       reforgedPlayerMetadata,
       randomSeed,
+      selectMode,
       gameName,
       startSpotCount,
     };
@@ -128,7 +153,7 @@ export default class MetadataParser extends StatefulBufferParser {
     for (let i = 0; i < count; i++) {
       const record: Partial<SlotRecord> = {};
       record.playerId = this.readUInt8();
-      this.skip(1);
+      record.downloadProgress = this.readUInt8();
       record.slotStatus = this.readUInt8();
       record.computerFlag = this.readUInt8();
       record.teamId = this.readUInt8();
@@ -143,7 +168,9 @@ export default class MetadataParser extends StatefulBufferParser {
 
   private parseReforgedPlayerMetadata(): ReforgedPlayerMetadata[] {
     const result: ReforgedPlayerMetadata[] = [];
-    while (this.readUInt8() === 0x39) {
+    const skinSet: Map<number, SkinData[]> = new Map();
+    while (this.peekUInt8() === 0x38 || this.peekUInt8() === 0x39) {
+      this.skip(1);
       const subtype = this.readUInt8();
       const followingBytes = this.readUInt32LE();
       const data = this.buffer.subarray(
@@ -163,11 +190,24 @@ export default class MetadataParser extends StatefulBufferParser {
           playerId: decoded.playerId,
           name: decoded.battleTag,
           clan: decoded.clan,
+          skins: [],
         });
       } else if (subtype === 0x4) {
+        const decoded = protoSkin.decode(data) as unknown as {
+          playerId: number;
+          skins: SkinData[];
+        };
+        if (decoded.skins !== undefined) {
+          skinSet.set(decoded.playerId, decoded.skins);
+        }
       }
       this.skip(followingBytes);
     }
+    for (const player of result) {
+      if (skinSet.has(player.playerId)) {
+        player.skins = skinSet.get(player.playerId)!;
+      }
+    }
     return result;
   }
 
@@ -220,13 +260,7 @@ export default class MetadataParser extends StatefulBufferParser {
     const playerId = this.readUInt8();
     const playerName = this.readZeroTermString("utf-8");
     const addData = this.readUInt8();
-    if (addData === 1) {
-      this.skip(1);
-    } else if (addData === 2) {
-      this.skip(2);
-    } else if (addData === 8) {
-      this.skip(8);
-    }
+    this.skip(addData);
     return { playerId, playerName };
   }
 
diff --git a/src/parsers/RawParser.ts b/src/parsers/RawParser.ts
index f63498fc..ab118292 100644
--- a/src/parsers/RawParser.ts
+++ b/src/parsers/RawParser.ts
@@ -1,4 +1,6 @@
 import StatefulBufferParser from "./StatefulBufferParser";
+import { inflate, constants } from "zlib";
+
 export type Header = {
   compressedSize: number;
   headerVersion: string;
@@ -25,6 +27,31 @@ export type DataBlock = {
   blockContent: Buffer;
 };
 
+const inflatePromise = (buffer: Buffer, options = {}): Promise<Buffer> =>
+  new Promise((resolve, reject) => {
+    inflate(buffer, options, (err, result) => {
+      if (err !== null) {
+        reject(err);
+      }
+      resolve(result);
+    });
+  });
+
+export async function getUncompressedData(
+  blocks: DataBlock[],
+): Promise<Buffer> {
+  const buffs: Buffer[] = [];
+  for (const block of blocks) {
+    const block2 = await inflatePromise(block.blockContent, {
+      finishFlush: constants.Z_SYNC_FLUSH,
+    });
+    if (block2.byteLength > 0 && block.blockContent.byteLength > 0) {
+      buffs.push(block2);
+    }
+  }
+  return Buffer.concat(buffs);
+}
+
 export default class CustomReplayParser extends StatefulBufferParser {
   private header: Header;
   private subheader: SubHeader;
diff --git a/src/parsers/StatefulBufferParser.ts b/src/parsers/StatefulBufferParser.ts
index c88c1699..e718833f 100644
--- a/src/parsers/StatefulBufferParser.ts
+++ b/src/parsers/StatefulBufferParser.ts
@@ -77,6 +77,11 @@ export default class StatefulBufferParser {
     return val;
   }
 
+  public peekUInt8(): number {
+    const val = this.buffer.readUInt8(this.offset);
+    return val;
+  }
+
   public readFloatLE(): number {
     const val = this.buffer.readFloatLE(this.offset);
     this.offset += 4;
diff --git a/test/replays/132/replays.test.ts b/test/replays/132/replays.test.ts
index 6934aa19..567157e4 100644
--- a/test/replays/132/replays.test.ts
+++ b/test/replays/132/replays.test.ts
@@ -1,6 +1,9 @@
 import W3GReplay from "../../../src/";
 import path from "path";
-import { GameDataBlock } from "../../../src/parsers/GameDataParser";
+import {
+  GameDataBlock,
+  TimeslotBlock,
+} from "../../../src/parsers/GameDataParser";
 
 const Parser = new W3GReplay();
 it("parses a reforged replay properly #1", async () => {
@@ -58,7 +61,7 @@ it("parses a netease 1.32 replay successfully", async () => {
 
 it("parse is a promise that resolves with parser output", async () => {
   const Parser = new W3GReplay();
-  const timeslotBlocks = [];
+  const timeslotBlocks: TimeslotBlock[] = [];
   let completedAsyncDummyTask = false;
   const metadataCallback = jest.fn();
   Parser.on("basic_replay_information", metadataCallback);
diff --git a/test/replays/200/2.0.2-LAN-bots.w3g b/test/replays/200/2.0.2-LAN-bots.w3g
new file mode 100644
index 00000000..3b6f8e5c
Binary files /dev/null and b/test/replays/200/2.0.2-LAN-bots.w3g differ
diff --git a/test/replays/200/replays.test.ts b/test/replays/200/replays.test.ts
index a3a0fffb..27bef44c 100644
--- a/test/replays/200/replays.test.ts
+++ b/test/replays/200/replays.test.ts
@@ -1,6 +1,6 @@
-import W3GReplay from "../../../src";
+import { readFileSync } from "fs";
+import W3GReplay, { MetadataParser, RawParser } from "../../../src";
 import path from "path";
-
 const Parser = new W3GReplay();
 
 it("recognizes a 'build haunted gold mine' command correctly and adds it to the player's buildings", async () => {
@@ -91,3 +91,20 @@ it("detects retraining", async () => {
   ]);
   expect(test.players[1].heroes[0].level).toBe(6);
 });
+
+it("parses 2.0.2 replay Reforged data successfully and without logging errors", async () => {
+  const consoleSpy = jest.spyOn(console, "log");
+
+  const rawParser = new RawParser();
+  const file = readFileSync(path.resolve(__dirname, "2.0.2-LAN-bots.w3g"));
+  const data = await rawParser.parse(file);
+
+  const metadataParser = new MetadataParser();
+  const metadata = await metadataParser.parse(data.blocks);
+
+  expect(metadata.reforgedPlayerMetadata).toBeDefined();
+  expect(metadata.reforgedPlayerMetadata.length).toBeGreaterThan(0);
+
+  expect(data.subheader.version).toBe(10100);
+  expect(consoleSpy).not.toHaveBeenCalled();
+});


Reviews:
user: login: PBug90
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-19T13:25:31Z
user: login: BogdanW3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T13:31:39Z
user: login: PBug90
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-19T14:47:52Z
user: login: PBug90
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-19T17:38:43Z
user: login: BogdanW3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T19:32:01Z
user: login: PBug90
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-19T19:42:11Z

Review comments:
diff_hunk: @@ -95,29 +110,44 @@ export default class MetadataParser extends StatefulBufferParser {
     const mapMetadata = this.parseEncodedMapMetaString(
       this.decodeGameMetaString(encodedString),
     );
-    this.skip(12);
+    const playerCount = this.readUInt32LE();
+    const gameType = this.readStringOfLength(4, "hex");
+    const localeHash = this.readStringOfLength(4, "hex");
     const playerListFinal = playerRecords.concat(
       playerRecords,
       this.parsePlayerList(),
     );
     let reforgedPlayerMetadata: ReforgedPlayerMetadata[] = [];
-    if (this.readUInt8() !== 25) {
-      this.skip(-1);
+    if (this.peekUInt8() !== 25) {
       reforgedPlayerMetadata = this.parseReforgedPlayerMetadata();
     }
-    this.skip(2);
+    if (this.readUInt8() !== 25) {
+      console.error("Unknown chunk detected!");
+      console.error(this.buffer.subarray(this.getOffset() - 1));
+    }
+    const remainingBytes = this.readUInt16LE();
     const slotRecordCount = this.readUInt8();
+    // remaining bytes are: slotRecordCount(1), slots(9*count), seed(4), mode(1), spots(1)
+    if (remainingBytes != 1 + slotRecordCount * 9 + 6) {
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: Can we use !== here instead? Not sure why the linter didnt error on this, but we use strict compare everywhere in this repo.
created_at: 2025-04-19T13:22:01Z
author_association: OWNER
diff_hunk: @@ -143,7 +173,12 @@ export default class MetadataParser extends StatefulBufferParser {
 
   private parseReforgedPlayerMetadata(): ReforgedPlayerMetadata[] {
     const result: ReforgedPlayerMetadata[] = [];
-    while (this.readUInt8() === 0x39) {
+    const skinSet: Map<number, number[]> = new Map();
+    while (this.peekUInt8() == 0x38 || this.peekUInt8() == 0x39) {
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: Same here, === instead of ==
created_at: 2025-04-19T13:22:18Z
author_association: OWNER
diff_hunk: @@ -143,7 +173,12 @@ export default class MetadataParser extends StatefulBufferParser {
 
   private parseReforgedPlayerMetadata(): ReforgedPlayerMetadata[] {
     const result: ReforgedPlayerMetadata[] = [];
-    while (this.readUInt8() === 0x39) {
+    const skinSet: Map<number, number[]> = new Map();
+    while (this.peekUInt8() == 0x38 || this.peekUInt8() == 0x39) {
+      const type = this.readUInt8();
+      if (type == 0x38) {
+        console.log("Message Type 0x38 detected (Patch 2.0.2)!");
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: Does this serve any other purpose than debugging? If you have a replay that matches this type, please provide a test case instead and remove the logging.
created_at: 2025-04-19T13:24:35Z
author_association: OWNER
diff_hunk: @@ -143,7 +173,12 @@ export default class MetadataParser extends StatefulBufferParser {
 
   private parseReforgedPlayerMetadata(): ReforgedPlayerMetadata[] {
     const result: ReforgedPlayerMetadata[] = [];
-    while (this.readUInt8() === 0x39) {
+    const skinSet: Map<number, number[]> = new Map();
+    while (this.peekUInt8() == 0x38 || this.peekUInt8() == 0x39) {
+      const type = this.readUInt8();
+      if (type == 0x38) {
+        console.log("Message Type 0x38 detected (Patch 2.0.2)!");
path: src/parsers/MetadataParser.ts
user: login: BogdanW3
body: Hmm... that's a good shout - I had the idea to log this as it's not a regular situation, but that depends on what Blizzard does in the next patch. I'll remove it and add a test to cover this case
created_at: 2025-04-19T13:31:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,29 +115,44 @@ export default class MetadataParser extends StatefulBufferParser {
     const mapMetadata = this.parseEncodedMapMetaString(
       this.decodeGameMetaString(encodedString),
     );
-    this.skip(12);
+    const playerCount = this.readUInt32LE();
+    const gameType = this.readStringOfLength(4, "hex");
+    const localeHash = this.readStringOfLength(4, "hex");
     const playerListFinal = playerRecords.concat(
       playerRecords,
       this.parsePlayerList(),
     );
     let reforgedPlayerMetadata: ReforgedPlayerMetadata[] = [];
-    if (this.readUInt8() !== 25) {
-      this.skip(-1);
+    if (this.peekUInt8() !== 25) {
       reforgedPlayerMetadata = this.parseReforgedPlayerMetadata();
     }
-    this.skip(2);
+    if (this.readUInt8() !== 25) {
+      console.error("Unknown chunk detected!");
+      console.error(this.buffer.subarray(this.getOffset() - 1));
+    }
+    const remainingBytes = this.readUInt16LE();
     const slotRecordCount = this.readUInt8();
+    // remaining bytes are: slotRecordCount(1), slots(9*count), seed(4), mode(1), spots(1)
+    if (remainingBytes !== 1 + slotRecordCount * 9 + 6) {
+      console.error(
+        `Remaining bytes (${remainingBytes}) do not match expected bytes (${1 + slotRecordCount * 9 + 6})`,
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: You are asserting console.log not to have been called in the test, but you use .error here. Can we match the error outputs to use console.log because thats how the lib currently logs all errors. I am aware that this is not good, and I am planning to change that in the future. 
created_at: 2025-04-19T14:46:26Z
author_association: OWNER
diff_hunk: @@ -95,29 +115,44 @@ export default class MetadataParser extends StatefulBufferParser {
     const mapMetadata = this.parseEncodedMapMetaString(
       this.decodeGameMetaString(encodedString),
     );
-    this.skip(12);
+    const playerCount = this.readUInt32LE();
+    const gameType = this.readStringOfLength(4, "hex");
+    const localeHash = this.readStringOfLength(4, "hex");
     const playerListFinal = playerRecords.concat(
       playerRecords,
       this.parsePlayerList(),
     );
     let reforgedPlayerMetadata: ReforgedPlayerMetadata[] = [];
-    if (this.readUInt8() !== 25) {
-      this.skip(-1);
+    if (this.peekUInt8() !== 25) {
       reforgedPlayerMetadata = this.parseReforgedPlayerMetadata();
     }
-    this.skip(2);
+    if (this.readUInt8() !== 25) {
+      console.error("Unknown chunk detected!");
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: Instead of using two log statements, can we use one and just concat the buffer to the string? Just use + this.buffer.subarray(...).toString("hex")
created_at: 2025-04-19T14:47:34Z
author_association: OWNER
diff_hunk: @@ -75,7 +86,8 @@ type MapMetadata = {
 
 export default class MetadataParser extends StatefulBufferParser {
   private mapmetaParser: StatefulBufferParser = new StatefulBufferParser();
-  async parse(blocks: DataBlock[]): Promise<ReplayMetadata> {
+
+  static async getUncompressedData(blocks: DataBlock[]): Promise<Buffer> {
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: What is the reasoning of using a static method instead of an instance method here?
created_at: 2025-04-19T17:38:43Z
author_association: OWNER
diff_hunk: @@ -75,7 +86,8 @@ type MapMetadata = {
 
 export default class MetadataParser extends StatefulBufferParser {
   private mapmetaParser: StatefulBufferParser = new StatefulBufferParser();
-  async parse(blocks: DataBlock[]): Promise<ReplayMetadata> {
+
+  static async getUncompressedData(blocks: DataBlock[]): Promise<Buffer> {
path: src/parsers/MetadataParser.ts
user: login: BogdanW3
body: It doesn't really depend on the metadata parser, it might even make sense to export it as an independent utility method?
created_at: 2025-04-19T19:32:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,7 +86,8 @@ type MapMetadata = {
 
 export default class MetadataParser extends StatefulBufferParser {
   private mapmetaParser: StatefulBufferParser = new StatefulBufferParser();
-  async parse(blocks: DataBlock[]): Promise<ReplayMetadata> {
+
+  static async getUncompressedData(blocks: DataBlock[]): Promise<Buffer> {
path: src/parsers/MetadataParser.ts
user: login: PBug90
body: Yes, that is a good idea.
created_at: 2025-04-19T19:42:11Z
author_association: OWNER

PR comments:
user: login: PBug90
created_at: 2025-04-19T12:57:51Z
body: Thank you for your contribution! Can you fix the linting issue (npm run lint)?
author_association: OWNER
user: login: BogdanW3
created_at: 2025-04-19T14:42:24Z
body: Linting again, sorry
author_association: CONTRIBUTOR
user: login: aaronjewell
created_at: 2025-04-20T07:02:09Z
body: The changes in #198 (and my suggested change)  are also needed to resolve some of the missing action console.log errors, as well as the reading out of buffer bounds at the end of the action subarrays, similar to #191
author_association: CONTRIBUTOR
user: login: PBug90
created_at: 2025-04-20T11:41:28Z
body: Can you elaborate on your changes to the actionparser? Did Blizzard add more metadata to replay files?

I tried to adhere to the old deepnode w3g_actions.txt file and you now introduced different naming for some action properties.
author_association: OWNER
user: login: BogdanW3
created_at: 2025-04-20T12:00:08Z
body: The changes to the action parser are based on Blizzard's namings from their own logs, but I tried to change as little data as I can:
- W3MMD isn't the real name of the message, W3MMD just abuses the cache loads
- most of the ids are either not ids but real object handles (NetTag), or at least not itemIds, so renames were introduced there

Other than that I mostly tried not to change much, but there will be breaking changes to adhere more closely to the game's actual usages for the fields that weren't correctly documented in the old w3g_XX.txt files.

All of the format changes are based on the data from 2.0.1, so there might be concerns there, indeed

author_association: CONTRIBUTOR
user: login: PBug90
created_at: 2025-04-20T13:18:05Z
body: > The changes to the action parser are based on Blizzard's namings from their own logs, but I tried to change as little data as I can:
> - W3MMD isn't the real name of the message, W3MMD just abuses the cache loads
> - most of the ids are either not ids but real object handles (NetTag), or at least not itemIds, so renames were introduced there
> 
> Other than that I mostly tried not to change much, but there will be breaking changes to adhere more closely to the game's actual usages for the fields that weren't correctly documented in the old w3g_XX.txt files.
> 
> All of the format changes are based on the data from 2.0.1, so there might be concerns there, indeed
> 

I see, thank you. Let me know when your work is done here, looks good so far.

If you speak of logs, is there any source for that that we can reference?
author_association: OWNER
user: login: BogdanW3
created_at: 2025-04-20T14:03:22Z
body: Nothing easily accessible, in theory you could hijack a function from one of the non-obfuscated builds of the game, either before 1.30 or the 2.0.1 that was released with all debugging data; but at that point it might be simpler to just open the game in a decompiler, albeit that gets into some grey territory depending on where you're from 😅 

I went to the 2.0.1 build for this, as it is among the best we can get for modern versions. Most of the unknown actions seem to be custom map events and actions, so it may be possible to create more tests in the future to verify all of this more.

As for when I think the work is done... I'm trying to apply this research to fixing Flo replays, but I am also happy that no w3gjs jest tests started failing! It may be in a state that's good to merge already. 


Thank you for being patient with me and all my linter errors! ❤️ 
author_association: CONTRIBUTOR