public static void Parse(Replay replay, byte[] buffer)
        {
            // Referenced from https://raw.githubusercontent.com/Blizzard/heroprotocol/master/protocol39445.py

            var gameEvents   = new List <GameEvent>();
            var ticksElapsed = 0;

            using (var stream = new MemoryStream(buffer))
            {
                var bitReader = new Streams.BitReader(stream);
                while (!bitReader.EndOfStream)
                {
                    var gameEvent = new GameEvent();
                    ticksElapsed          += (int)bitReader.Read(6 + (bitReader.Read(2) << 3));
                    gameEvent.ticksElapsed = ticksElapsed;
                    var playerIndex = (int)bitReader.Read(5);
                    if (playerIndex == 16)
                    {
                        gameEvent.isGlobal = true;
                    }
                    else
                    {
                        gameEvent.player = replay.ClientList[playerIndex];
                    }

                    gameEvent.eventType = (GameEventType)bitReader.Read(7);
                    switch (gameEvent.eventType)
                    {
                    case GameEventType.CStartGameEvent:
                        break;

                    case GameEventType.CUserFinishedLoadingSyncEvent:
                        break;

                    case GameEventType.CUserOptionsEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_gameFullyDownloaded
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_developmentCheatsEnabled
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_testCheatsEnabled
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_multiplayerCheatsEnabled
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_syncChecksummingEnabled
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_isMapToMapTransition
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_debugPauseEnabled
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_useGalaxyAsserts
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_platformMac
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },                                                              // m_cameraFollow
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(32)
                                },                                                              // m_baseBuildNum
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(32)
                                },                                                              // m_buildNum
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(32)
                                },                                                              // m_versionFlags
                                new TrackerEventStructure {
                                    DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(9)
                                }                                                                                          /* m_hotkeyProfile */
                            }
                        };
                        break;

                    case GameEventType.CBankFileEvent:
                        gameEvent.data = new TrackerEventStructure {
                            DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7)
                        };
                        break;

                    case GameEventType.CBankSectionEvent:
                        gameEvent.data = new TrackerEventStructure {
                            DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6)
                        };
                        break;

                    case GameEventType.CBankKeyEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6)
                                },
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(32)
                                },
                                new TrackerEventStructure {
                                    DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7)
                                }
                            }
                        };
                        break;

                    case GameEventType.CBankSignatureEvent:
                        gameEvent.data = new TrackerEventStructure {
                            DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)]
                        };
                        for (var i = 0; i < gameEvent.data.array.Length; i++)
                        {
                            gameEvent.data.array[i] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(8)
                            }
                        }
                        ;
                        gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7);
                        break;

                    case GameEventType.CCameraSaveEvent:
                        bitReader.Read(3);     // m_which
                        bitReader.Read(16);    // x
                        bitReader.Read(16);    // y
                        break;

                    case GameEventType.CCommandManagerResetEvent:
                        bitReader.Read(32);     // m_sequence
                        break;

                    case GameEventType.CCmdEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new TrackerEventStructure[5]
                        };

                        // m_cmdFlags
                        if (replay.ReplayBuild < 33684)
                        {
                            gameEvent.data.array[0] = new TrackerEventStructure {
                                array = new TrackerEventStructure[22]
                            }
                        }
                        ;
                        else if (replay.ReplayBuild < 37117)
                        {
                            gameEvent.data.array[0] = new TrackerEventStructure {
                                array = new TrackerEventStructure[23]
                            }
                        }
                        ;
                        else if (replay.ReplayBuild < 38236)
                        {
                            gameEvent.data.array[0] = new TrackerEventStructure {
                                array = new TrackerEventStructure[24]
                            }
                        }
                        ;
                        else
                        {
                            gameEvent.data.array[0] = new TrackerEventStructure {
                                array = new TrackerEventStructure[25]
                            }
                        };

                        for (var i = 0; i < gameEvent.data.array[0].array.Length; i++)
                        {
                            gameEvent.data.array[0].array[i] = new TrackerEventStructure {
                                DataType = 7, unsignedInt = bitReader.Read(1)
                            }
                        }
                        ;

                        // m_abil
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[1] = new TrackerEventStructure {
                                array = new[] {
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(16)
                                    },                                                              // m_abilLink
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(5)
                                    },                                                             // m_abilCmdIndex
                                    new TrackerEventStructure()
                                }
                            };
                            if (bitReader.ReadBoolean())
                            {
                                // m_abilCmdData
                                gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8);
                            }
                        }

                        // m_data
                        switch (bitReader.Read(2))
                        {
                        case 0:         // None
                            break;

                        case 1:         // TargetPoint
                            gameEvent.data.array[2] = new TrackerEventStructure {
                                array = new[] { new TrackerEventStructure {
                                                    unsignedInt = bitReader.Read(20)
                                                }, new TrackerEventStructure {
                                                    unsignedInt = bitReader.Read(20)
                                                }, new TrackerEventStructure {
                                                    vInt = bitReader.Read(32) - 2147483648
                                                } }
                            };
                            break;

                        case 2:         // TargetUnit
                            gameEvent.data.array[2] = new TrackerEventStructure {
                                array = new[] {
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(16)
                                    },                                                                  // m_targetUnitFlags
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(8)
                                    },                                                                 // m_timer
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(32)
                                    },                                                                  // m_tag
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(16)
                                    },                                                                  // m_snapshotUnitLink
                                    new TrackerEventStructure(),
                                    new TrackerEventStructure(),
                                    new TrackerEventStructure(),
                                }
                            };
                            if (bitReader.ReadBoolean())
                            {
                                // m_snapshotControlPlayerId
                                gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4);
                            }
                            if (bitReader.ReadBoolean())
                            {
                                // m_snapshotUpkeepPlayerId
                                gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4);
                            }

                            // m_snapshotPoint
                            gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure {
                                                                                 unsignedInt = bitReader.Read(20)
                                                                             }, new TrackerEventStructure {
                                                                                 unsignedInt = bitReader.Read(20)
                                                                             }, new TrackerEventStructure {
                                                                                 vInt = bitReader.Read(32) - 2147483648
                                                                             } };
                            break;

                        case 3:         // Data
                            gameEvent.data.array[2] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            };
                            break;
                        }

                        if (replay.ReplayBuild >= 33684)
                        {
                            bitReader.Read(32);     // m_sequence
                        }
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[3] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            }
                        }
                        ;                                                                                                 // m_otherUnit
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[4] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            }
                        }
                        ;                                                                                                 // m_unitGroup
                        break;

                    case GameEventType.CSelectionDeltaEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(4)
                                },                                                             // m_controlGroupId

                                // m_delta
                                new TrackerEventStructure {
                                    array = new[] {
                                        new TrackerEventStructure {
                                            unsignedInt = bitReader.Read(9)
                                        },                                                         // m_subgroupIndex
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure()
                                    }
                                }
                            }
                        };

                        // m_removeMask
                        switch (bitReader.Read(2))
                        {
                        case 0:         // None
                            break;

                        case 1:         // Mask
                            bitReader.Read(bitReader.Read(9));
                            break;

                        case 2:         // OneIndices
                        case 3:         // ZeroIndices
                            gameEvent.data.array[1].array[1] = new TrackerEventStructure {
                                array = new TrackerEventStructure[bitReader.Read(9)]
                            };
                            for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++)
                            {
                                gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(9)
                                }
                            }
                            ;
                            break;
                        }

                        // m_addSubgroups
                        gameEvent.data.array[1].array[2] = new TrackerEventStructure {
                            array = new TrackerEventStructure[bitReader.Read(9)]
                        };
                        for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++)
                        {
                            gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure {
                                array = new[] {
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(16)
                                    },                                                              // m_unitLink
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(8)
                                    },                                                             // m_subgroupPriority
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(8)
                                    },                                                             // m_intraSubgroupPriority
                                    new TrackerEventStructure {
                                        unsignedInt = bitReader.Read(9)
                                    }
                                }
                            }
                        }
                        ;                                                                              // m_count

                        // m_addUnitTags
                        gameEvent.data.array[1].array[3] = new TrackerEventStructure {
                            array = new TrackerEventStructure[bitReader.Read(9)]
                        };
                        for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++)
                        {
                            gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            }
                        }
                        ;
                        break;

                    case GameEventType.CControlGroupUpdateEvent:
                        bitReader.Read(4);     // m_controlGroupIndex

                        // m_controlGroupUpdate
                        if (replay.ReplayBuild < 36359)     // Not sure exactly when this change happened - roughly around here.  This primarily affected 'The Lost Vikings' hero
                        {
                            bitReader.Read(2);
                        }
                        else
                        {
                            bitReader.Read(3);
                        }

                        // m_mask
                        switch (bitReader.Read(2))
                        {
                        case 0:         // None
                            break;

                        case 1:         // Mask
                            bitReader.Read(bitReader.Read(9));
                            break;

                        case 2:         // OneIndices
                        case 3:         // ZeroIndices
                            gameEvent.data.array[1].array[1] = new TrackerEventStructure {
                                array = new TrackerEventStructure[bitReader.Read(9)]
                            };
                            for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++)
                            {
                                gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(9)
                                }
                            }
                            ;
                            break;
                        }
                        break;

                    case GameEventType.CResourceTradeEvent:
                        bitReader.Read(4);     // m_recipientId
                        bitReader.Read(32);    // m_resources, should be offset -2147483648
                        bitReader.Read(32);    // m_resources, should be offset -2147483648
                        bitReader.Read(32);    // m_resources, should be offset -2147483648
                        break;

                    case GameEventType.CTriggerChatMessageEvent:
                        gameEvent.data = new TrackerEventStructure {
                            DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10)
                        };
                        break;

                    case GameEventType.CTriggerPingEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(32) - 2147483648
                                },
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(32) - 2147483648
                                },
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(32)
                                },
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(1)
                                },
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(32) - 2147483648
                                }
                            }
                        };
                        break;

                    case GameEventType.CUnitClickEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(32)
                        };                                                                                   // m_unitTag
                        break;

                    case GameEventType.CTriggerSkippedEvent:
                        break;

                    case GameEventType.CTriggerSoundLengthQueryEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                unsignedInt = bitReader.Read(32)
                                            }, new TrackerEventStructure {
                                                unsignedInt = bitReader.Read(32)
                                            } }
                        };
                        break;

                    case GameEventType.CTriggerSoundOffsetEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(32)
                        };
                        break;

                    case GameEventType.CTriggerTransmissionOffsetEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                vInt = bitReader.Read(32) - 2147483648
                                            }, new TrackerEventStructure {
                                                unsignedInt = bitReader.Read(32)
                                            } }
                        };
                        break;

                    case GameEventType.CTriggerTransmissionCompleteEvent:
                        gameEvent.data = new TrackerEventStructure {
                            vInt = bitReader.Read(32) - 2147483648
                        };
                        break;

                    case GameEventType.CCameraUpdateEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new TrackerEventStructure[6]
                        };
                        if (bitReader.ReadBoolean())
                        {
                            // m_target, x/y
                            gameEvent.data.array[0] = new TrackerEventStructure {
                                array = new[] { new TrackerEventStructure {
                                                    unsignedInt = bitReader.Read(16)
                                                }, new TrackerEventStructure {
                                                    unsignedInt = bitReader.Read(16)
                                                } }
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            // m_distance
                            gameEvent.data.array[1] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(16)
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            // m_pitch
                            gameEvent.data.array[2] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(16)
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            // m_yaw
                            gameEvent.data.array[3] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(16)
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            // m_reason
                            gameEvent.data.array[4] = new TrackerEventStructure {
                                vInt = bitReader.Read(8) - 128
                            }
                        }
                        ;

                        // m_follow
                        gameEvent.data.array[5] = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(1)
                        };
                        break;

                    case GameEventType.CTriggerPlanetMissionLaunchedEvent:
                        bitReader.Read(32);     // m_difficultyLevel, offset -2147483648
                        break;

                    case GameEventType.CTriggerDialogControlEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(32)                         /* Actually signed - not handled correctly */
                                },
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(32)                         /* Actually signed - not handled correctly */
                                },
                                new TrackerEventStructure()
                            }
                        };
                        switch (bitReader.Read(3))
                        {
                        case 0:         // None
                            break;

                        case 1:         // Checked
                            gameEvent.data.array[2].unsignedInt = bitReader.Read(1);
                            break;

                        case 2:         // ValueChanged
                            gameEvent.data.array[2].unsignedInt = bitReader.Read(32);
                            break;

                        case 3:                                                // SelectionChanged
                            gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */
                            break;

                        case 4:         // TextChanged
                            gameEvent.data.array[2].DataType = 2;
                            gameEvent.data.array[2].blob     = bitReader.ReadBlobPrecededWithLength(11);
                            break;

                        case 5:         // MouseButton
                            gameEvent.data.array[2].unsignedInt = bitReader.Read(32);
                            break;
                        }
                        break;

                    case GameEventType.CTriggerSoundLengthSyncEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new TrackerEventStructure[2]
                        };
                        gameEvent.data.array[0] = new TrackerEventStructure {
                            array = new TrackerEventStructure[bitReader.Read(7)]
                        };
                        for (var i = 0; i < gameEvent.data.array[0].array.Length; i++)
                        {
                            gameEvent.data.array[0].array[i] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            }
                        }
                        ;
                        gameEvent.data.array[1] = new TrackerEventStructure {
                            array = new TrackerEventStructure[bitReader.Read(7)]
                        };
                        for (var i = 0; i < gameEvent.data.array[1].array.Length; i++)
                        {
                            gameEvent.data.array[1].array[i] = new TrackerEventStructure {
                                unsignedInt = bitReader.Read(32)
                            }
                        }
                        ;
                        break;

                    case GameEventType.CTriggerConversationSkippedEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(1)
                        };
                        break;

                    case GameEventType.CTriggerMouseClickedEvent:
                        bitReader.Read(32);      // m_button
                        bitReader.ReadBoolean(); // m_down
                        bitReader.Read(11);      // m_posUI X
                        bitReader.Read(11);      // m_posUI Y
                        bitReader.Read(20);      // m_posWorld X
                        bitReader.Read(20);      // m_posWorld Y
                        bitReader.Read(32);      // m_posWorld Z (Offset -2147483648)
                        bitReader.Read(8);       // m_flags (-128)
                        break;

                    case GameEventType.CTriggerMouseMovedEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] {
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(11)
                                },
                                new TrackerEventStructure {
                                    unsignedInt = bitReader.Read(11)
                                },
                                new TrackerEventStructure {
                                    array = new[] { new TrackerEventStructure {
                                                        unsignedInt = bitReader.Read(20)
                                                    }, new TrackerEventStructure {
                                                        unsignedInt = bitReader.Read(20)
                                                    }, new TrackerEventStructure {
                                                        vInt = bitReader.Read(32) - 2147483648
                                                    } }
                                },
                                new TrackerEventStructure {
                                    vInt = bitReader.Read(8) - 128
                                }
                            }
                        };
                        break;

                    case GameEventType.CTriggerHotkeyPressedEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(32)
                        };                                                                                   // May be missing an offset value
                        break;

                    case GameEventType.CTriggerTargetModeUpdateEvent:
                        bitReader.Read(16);    // m_abilLink
                        bitReader.Read(5);     // m_abilCmdIndex
                        bitReader.Read(8);     // m_state (-128)
                        break;

                    case GameEventType.CTriggerSoundtrackDoneEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(32)
                        };
                        break;

                    case GameEventType.CTriggerKeyPressedEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                vInt = bitReader.Read(8) - 128
                                            }, new TrackerEventStructure {
                                                vInt = bitReader.Read(8) - 128
                                            } }
                        };
                        break;

                    case GameEventType.CTriggerCutsceneBookmarkFiredEvent:
                        // m_cutsceneId, m_bookmarkName
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                vInt = bitReader.Read(32) - 2147483648
                                            }, new TrackerEventStructure {
                                                DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7)
                                            } }
                        };
                        break;

                    case GameEventType.CTriggerCutsceneEndSceneFiredEvent:
                        // m_cutsceneId
                        gameEvent.data = new TrackerEventStructure {
                            vInt = bitReader.Read(32) - 2147483648
                        };
                        break;

                    case GameEventType.CGameUserLeaveEvent:
                        break;

                    case GameEventType.CGameUserJoinEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new TrackerEventStructure[5]
                        };
                        gameEvent.data.array[0] = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(2)
                        };
                        gameEvent.data.array[1] = new TrackerEventStructure {
                            DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8)
                        };
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[2] = new TrackerEventStructure {
                                DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7)
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[3] = new TrackerEventStructure {
                                DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8)
                            }
                        }
                        ;
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[4] = new TrackerEventStructure {
                                DataType = 2, blob = bitReader.ReadBytes(40)
                            }
                        }
                        ;
                        break;

                    case GameEventType.CCommandManagerStateEvent:
                        gameEvent.data = new TrackerEventStructure {
                            DataType = 7, unsignedInt = bitReader.Read(2)
                        };                                                                                                // m_state
                        if (replay.ReplayBuild >= 33684)
                        {
                            if (bitReader.ReadBoolean())
                            {
                                // m_sequence
                                gameEvent.data.array = new[] { new TrackerEventStructure {
                                                                   DataType = 9, vInt = bitReader.Read(8)
                                                               }, new TrackerEventStructure {
                                                                   DataType = 9, vInt = bitReader.Read(8)
                                                               }, new TrackerEventStructure {
                                                                   DataType = 9, vInt = bitReader.Read(16)
                                                               } }
                            }
                        }
                        ;
                        break;

                    case GameEventType.CCmdUpdateTargetPointEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                DataType = 7, unsignedInt = bitReader.Read(20)
                                            }, new TrackerEventStructure {
                                                DataType = 7, unsignedInt = bitReader.Read(20)
                                            }, new TrackerEventStructure {
                                                DataType = 9, vInt = bitReader.Read(32) - 2147483648
                                            } }
                        };
                        break;

                    case GameEventType.CCmdUpdateTargetUnitEvent:
                        gameEvent.data = new TrackerEventStructure {
                            array = new TrackerEventStructure[7]
                        };
                        gameEvent.data.array[0] = new TrackerEventStructure {
                            DataType = 7, unsignedInt = bitReader.Read(16)
                        };                                                                                                          // m_targetUnitFlags
                        gameEvent.data.array[1] = new TrackerEventStructure {
                            DataType = 7, unsignedInt = bitReader.Read(8)
                        };                                                                                                         // m_timer
                        gameEvent.data.array[2] = new TrackerEventStructure {
                            DataType = 7, unsignedInt = bitReader.Read(32)
                        };                                                                                                          // m_tag
                        gameEvent.data.array[3] = new TrackerEventStructure {
                            DataType = 7, unsignedInt = bitReader.Read(16)
                        };                                                                                                          // m_snapshotUnitLink
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[4] = new TrackerEventStructure {
                                DataType = 7, unsignedInt = bitReader.Read(4)
                            }
                        }
                        ;                                                                                                              // m_snapshotControlPlayerId
                        if (bitReader.ReadBoolean())
                        {
                            gameEvent.data.array[5] = new TrackerEventStructure {
                                DataType = 7, unsignedInt = bitReader.Read(4)
                            }
                        }
                        ;                                                                                                              // m_snapshotUpkeepPlayerId
                        gameEvent.data.array[6] = new TrackerEventStructure {
                            array = new[] { new TrackerEventStructure {
                                                DataType = 7, unsignedInt = bitReader.Read(20)
                                            }, new TrackerEventStructure {
                                                DataType = 7, unsignedInt = bitReader.Read(20)
                                            }, new TrackerEventStructure {
                                                DataType = 9, vInt = bitReader.Read(32) - 2147483648
                                            } }
                        };                                                                                                                                                                                                                                                                                                                            // m_snapshotPoint (x, y, z)
                        break;

                    case GameEventType.CHeroTalentSelectedEvent:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(32)
                        };                                                                                   // m_index
                        break;

                    case GameEventType.CHeroTalentTreeSelectionPanelToggled:
                        gameEvent.data = new TrackerEventStructure {
                            unsignedInt = bitReader.Read(1)
                        };                                                                                  // m_shown
                        break;

                    default:
                        throw new NotImplementedException();
                    }

                    bitReader.AlignToByte();
                    gameEvents.Add(gameEvent);
                }
            }

            replay.GameEvents = gameEvents;

            // Gather talent selections
            var talentGameEventsDictionary = replay.GameEvents
                                             .Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent)
                                             .GroupBy(i => i.player)
                                             .ToDictionary(
                i => i.Key,
                i => i.Select(j => new Tuple <int, TimeSpan>((int)j.data.unsignedInt.Value, j.TimeSpan)).OrderBy(j => j.Item2).ToArray());

            foreach (var player in talentGameEventsDictionary.Keys)
            {
                player.Talents = talentGameEventsDictionary[player];
            }

            // Gather Team Level Milestones (From talent choices: 1 / 4 / 7 / 10 / 13 / 16 / 20)
            for (var currentTeam = 0; currentTeam < replay.TeamLevelMilestones.Length; currentTeam++)
            {
                var maxTalentChoices = replay.Players.Where(i => i.Team == currentTeam).Select(i => i.Talents.Length).Max();
                replay.TeamLevelMilestones[currentTeam] = new TimeSpan[maxTalentChoices];
                var appropriatePlayers = replay.Players.Where(j => j.Team == currentTeam && j.Talents.Length == maxTalentChoices);
                for (var i = 0; i < replay.TeamLevelMilestones[currentTeam].Length; i++)
                {
                    replay.TeamLevelMilestones[currentTeam][i] = appropriatePlayers.Select(j => j.Talents[i].Item2).Min();
                }
            }

            // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder

            /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) });
             * string eventGroupData = "";
             * foreach (var eventGroup in eventGroups)
             * {
             *  foreach (var eventData in eventGroup.Events)
             *      eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n";
             *  File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData);
             *  eventGroupData = "";
             * } */
        }
    }
        public static List<GameEvent> Parse(byte[] buffer, Player[] clientList, int replayBuild)
        {
            // Referenced from https://raw.githubusercontent.com/Blizzard/heroprotocol/master/protocol39445.py

            var gameEvents = new List<GameEvent>();

            var ticksElapsed = 0;
            using (var stream = new MemoryStream(buffer))
            {
                var bitReader = new Streams.BitReader(stream);
                while (!bitReader.EndOfStream)
                {
                    var gameEvent = new GameEvent();
                    ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3));
                    gameEvent.ticksElapsed = ticksElapsed;
                    var playerIndex = (int)bitReader.Read(5);
                    if (playerIndex == 16)
                        gameEvent.isGlobal = true;
                    else
                        gameEvent.player = clientList[playerIndex];

                    gameEvent.eventType = (GameEventType)bitReader.Read(7);
                    switch (gameEvent.eventType)
                    {
                        case GameEventType.CStartGameEvent:
                            break;
                        case GameEventType.CUserFinishedLoadingSyncEvent:
                            break;
                        case GameEventType.CUserOptionsEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_gameFullyDownloaded
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_developmentCheatsEnabled
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_testCheatsEnabled
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_multiplayerCheatsEnabled
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_syncChecksummingEnabled
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_isMapToMapTransition
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_debugPauseEnabled
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_useGalaxyAsserts
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_platformMac
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },  // m_cameraFollow
                                new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_baseBuildNum
                                new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_buildNum
                                new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_versionFlags
                                new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(9) } /* m_hotkeyProfile */ } };
                            break;
                        case GameEventType.CBankFileEvent:
                            gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) };
                            break;
                        case GameEventType.CBankSectionEvent:
                            gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) };
                            break;
                        case GameEventType.CBankKeyEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) },
                                new TrackerEventStructure { unsignedInt = bitReader.Read(32) },
                                new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } };
                            break;
                        case GameEventType.CBankSignatureEvent:
                            gameEvent.data = new TrackerEventStructure { DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)] };
                            for (var i = 0; i < gameEvent.data.array.Length; i++)
                                gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) };
                            gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7);
                            break;
                        case GameEventType.CCameraSaveEvent:
                            bitReader.Read(3); // m_which
                            bitReader.Read(16); // x
                            bitReader.Read(16); // y
                            break;
                        case GameEventType.CCommandManagerResetEvent:
                            bitReader.Read(32); // m_sequence
                            break;
                        case GameEventType.CGameCheatEvent:
                            // m_target
                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[4] };

                            switch (bitReader.Read(2))
                            {
                                case 0: // None
                                    break;
                                case 1: // TargetPoint
                                    gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } };
                                    break;
                                case 2: // TargetUnit
                                    gameEvent.data.array[0] = new TrackerEventStructure {
                                        array = new[] {
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(), } };
                                    if (bitReader.ReadBoolean())
                                        // m_snapshotControlPlayerId
                                        gameEvent.data.array[0].array[4].unsignedInt = bitReader.Read(4);
                                    if (bitReader.ReadBoolean())
                                        // m_snapshotUpkeepPlayerId
                                        gameEvent.data.array[0].array[5].unsignedInt = bitReader.Read(4);

                                    // m_snapshotPoint
                                    gameEvent.data.array[0].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } };
                                    break;
                            }

                            bitReader.Read(32); // m_time
                            Encoding.ASCII.GetString(bitReader.ReadBlobPrecededWithLength(10)); // m_verb
                            Encoding.ASCII.GetString(bitReader.ReadBlobPrecededWithLength(10)); // m_arguments
                            break;
                        case GameEventType.CCmdEvent:
                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] };

                            // m_cmdFlags
                            if (replayBuild < 33684)
                                gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[22] };
                            else if (replayBuild < 37117)
                                gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[23] };
                            else if (replayBuild < 38236)
                                gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[24] };
                            else
                                gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[25] };

                            for (var i = 0; i < gameEvent.data.array[0].array.Length; i++)
                                gameEvent.data.array[0].array[i] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(1) };

                            // m_abil
                            if (bitReader.ReadBoolean())
                            {
                                gameEvent.data.array[1] = new TrackerEventStructure {
                                    array = new[] {
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_abilLink
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, // m_abilCmdIndex
                                    new TrackerEventStructure() } };
                                if (bitReader.ReadBoolean())
                                    // m_abilCmdData
                                    gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8);
                            }

                            // m_data
                            switch (bitReader.Read(2))
                            {
                                case 0: // None
                                    break;
                                case 1: // TargetPoint
                                    gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } };
                                    break;
                                case 2: // TargetUnit
                                    gameEvent.data.array[2] = new TrackerEventStructure { array = new[] {
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag
                                        new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(),
                                        new TrackerEventStructure(), } };
                                    if (bitReader.ReadBoolean())
                                        // m_snapshotControlPlayerId
                                        gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4);
                                    if (bitReader.ReadBoolean())
                                        // m_snapshotUpkeepPlayerId
                                        gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4);

                                    // m_snapshotPoint
                                    gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } };
                                    break;
                                case 3: // Data
                                    gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                                    break;
                            }

                            if (replayBuild >= 33684)
                                bitReader.Read(32); // m_sequence
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_otherUnit
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitGroup
                            break;
                        case GameEventType.CSelectionDeltaEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, // m_controlGroupId

                                // m_delta
                                new TrackerEventStructure { array = new[] {
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, // m_subgroupIndex
                                    new TrackerEventStructure(),
                                    new TrackerEventStructure(),
                                    new TrackerEventStructure(),
                                    new TrackerEventStructure() } } }
                            };

                            // m_removeMask
                            switch (bitReader.Read(2))
                            {
                                case 0: // None
                                    break;
                                case 1: // Mask
                                    bitReader.Read(bitReader.Read(9));
                                    break;
                                case 2: // OneIndices
                                case 3: // ZeroIndices
                                    gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] };
                                    for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++)
                                        gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) };
                                    break;
                            }

                            // m_addSubgroups
                            gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] };
                            for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++)
                                gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] {
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_unitLink
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_subgroupPriority
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_intraSubgroupPriority
                                    new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } }; // m_count

                            // m_addUnitTags
                            gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] };
                            for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++)
                                gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                            break;
                        case GameEventType.CControlGroupUpdateEvent:
                            bitReader.Read(4); // m_controlGroupIndex

                            // m_controlGroupUpdate
                            if (replayBuild < 36359) // Not sure exactly when this change happened - roughly around here.  This primarily affected 'The Lost Vikings' hero
                                bitReader.Read(2);
                            else
                                bitReader.Read(3);

                            // m_mask
                            switch (bitReader.Read(2))
                            {
                                case 0: // None
                                    break;
                                case 1: // Mask
                                    bitReader.Read(bitReader.Read(9));
                                    break;
                                case 2: // OneIndices
                                case 3: // ZeroIndices
                                    gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] };
                                    for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++)
                                        gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) };
                                    break;
                            }
                            break;
                        case GameEventType.CSelectionSyncCheckEvent:
                            bitReader.Read(4); // m_controlGroupId

                            // m_selectionSyncData
                            bitReader.Read(9); // m_count
                            bitReader.Read(9); // m_subgroupCount
                            bitReader.Read(9); // m_activeSubgroupIndex
                            bitReader.Read(32); // m_unitTagsChecksum
                            bitReader.Read(32); // m_subgroupIndicesChecksum
                            bitReader.Read(32); // m_subgroupsChecksum
                            break;
                        case GameEventType.CResourceTradeEvent:
                            bitReader.Read(4); // m_recipientId
                            bitReader.Read(32); // m_resources, should be offset -2147483648
                            bitReader.Read(32); // m_resources, should be offset -2147483648
                            bitReader.Read(32); // m_resources, should be offset -2147483648
                            break;
                        case GameEventType.CTriggerChatMessageEvent:
                            gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10) };
                            break;
                        case GameEventType.CTriggerPingEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 },
                                new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 },
                                new TrackerEventStructure { unsignedInt = bitReader.Read(32) },
                                new TrackerEventStructure { unsignedInt = bitReader.Read(1) },
                                new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } };
                            break;
                        case GameEventType.CUnitClickEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitTag
                            break;
                        case GameEventType.CTriggerSkippedEvent:
                            break;
                        case GameEventType.CTriggerSoundLengthQueryEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } };
                            break;
                        case GameEventType.CTriggerSoundOffsetEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                            break;
                        case GameEventType.CTriggerTransmissionOffsetEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } };
                            break;
                        case GameEventType.CTriggerTransmissionCompleteEvent:
                            gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 };
                            break;
                        case GameEventType.CCameraUpdateEvent:
                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] };
                            if (bitReader.ReadBoolean())
                                // m_target, x/y
                                gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } };
                            if (bitReader.ReadBoolean())
                                // m_distance
                                gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) };
                            if (bitReader.ReadBoolean())
                                // m_pitch
                                gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) };
                            if (bitReader.ReadBoolean())
                                // m_yaw
                                gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) };
                            if (bitReader.ReadBoolean())
                                // m_reason
                                gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 };

                            // m_follow
                            gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) };
                            break;
                        case GameEventType.CTriggerPlanetMissionLaunchedEvent:
                            bitReader.Read(32); // m_difficultyLevel, offset -2147483648
                            break;
                        case GameEventType.CTriggerDialogControlEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ },
                                new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ },
                                new TrackerEventStructure() } };
                            switch (bitReader.Read(3))
                            {
                                case 0: // None
                                    break;
                                case 1: // Checked
                                    gameEvent.data.array[2].unsignedInt = bitReader.Read(1);
                                    break;
                                case 2: // ValueChanged
                                    gameEvent.data.array[2].unsignedInt = bitReader.Read(32);
                                    break;
                                case 3: // SelectionChanged
                                    gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */
                                    break;
                                case 4: // TextChanged
                                    gameEvent.data.array[2].DataType = 2;
                                    gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11);
                                    break;
                                case 5: // MouseButton
                                    gameEvent.data.array[2].unsignedInt = bitReader.Read(32);
                                    break;
                            }
                            break;
                        case GameEventType.CTriggerSoundLengthSyncEvent:
                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] };
                            gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] };
                            for (var i = 0; i < gameEvent.data.array[0].array.Length; i++)
                                gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                            gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] };
                            for (var i = 0; i < gameEvent.data.array[1].array.Length; i++)
                                gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                            break;
                        case GameEventType.CTriggerConversationSkippedEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) };
                            break;
                        case GameEventType.CTriggerMouseClickedEvent:
                            bitReader.Read(32); // m_button
                            bitReader.ReadBoolean(); // m_down
                            bitReader.Read(11); // m_posUI X
                            bitReader.Read(11); // m_posUI Y
                            bitReader.Read(20); // m_posWorld X
                            bitReader.Read(20); // m_posWorld Y
                            bitReader.Read(32); // m_posWorld Z (Offset -2147483648)
                            bitReader.Read(8); // m_flags (-128)
                            break;
                        case GameEventType.CTriggerMouseMovedEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] {
                                new TrackerEventStructure { unsignedInt = bitReader.Read(11) },
                                new TrackerEventStructure { unsignedInt = bitReader.Read(11) },
                                new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } },
                                new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } };
                            break;
                        case GameEventType.CTriggerHotkeyPressedEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value
                            break;
                        case GameEventType.CTriggerTargetModeUpdateEvent:
                            bitReader.Read(16); // m_abilLink
                            bitReader.Read(5); // m_abilCmdIndex
                            bitReader.Read(8); // m_state (-128)
                            break;
                        case GameEventType.CTriggerSoundtrackDoneEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) };
                            break;
                        case GameEventType.CTriggerKeyPressedEvent:
                            gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } };
                            break;
                        case GameEventType.CTriggerCutsceneBookmarkFiredEvent:
                            // m_cutsceneId, m_bookmarkName
                            gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } };
                            break;
                        case GameEventType.CTriggerCutsceneEndSceneFiredEvent:
                            // m_cutsceneId
                            gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 };
                            break;
                        case GameEventType.CGameUserLeaveEvent:
                            break;
                        case GameEventType.CGameUserJoinEvent:
                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] };
                            gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) };
                            gameEvent.data.array[1] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) };
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[2] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) };
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[3] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) };
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[4] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBytes(40) };
                            break;
                        case GameEventType.CCommandManagerStateEvent:
                            gameEvent.data = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(2) }; // m_state
                            if (replayBuild >= 33684)
                                if (bitReader.ReadBoolean())
                                    // m_sequence
                                    gameEvent.data.array = new[] { new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(16) } };
                            break;
                        case GameEventType.CCmdUpdateTargetPointEvent:
                            if (replayBuild >= 40336 && bitReader.ReadBoolean())
                                bitReader.Read(32);

                            gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } };
                            break;
                        case GameEventType.CCmdUpdateTargetUnitEvent:
                            if (replayBuild >= 40336 && bitReader.ReadBoolean())
                                bitReader.Read(32);

                            gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] };
                            gameEvent.data.array[0] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_targetUnitFlags
                            gameEvent.data.array[1] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(8) }; // m_timer
                            gameEvent.data.array[2] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(32) }; // m_tag
                            gameEvent.data.array[3] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_snapshotUnitLink
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[4] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotControlPlayerId
                            if (bitReader.ReadBoolean())
                                gameEvent.data.array[5] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotUpkeepPlayerId
                            gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; // m_snapshotPoint (x, y, z)
                            break;
                        case GameEventType.CHeroTalentSelectedEvent:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_index
                            break;
                        case GameEventType.CHeroTalentTreeSelectionPanelToggled:
                            gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; // m_shown
                            break;
                        default:
                            throw new NotImplementedException();
                    }

                    bitReader.AlignToByte();
                    gameEvents.Add(gameEvent);
                }
            }

            return gameEvents;

            // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder
            /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) });
            string eventGroupData = "";
            foreach (var eventGroup in eventGroups)
            {
                foreach (var eventData in eventGroup.Events)
                    eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n";
                File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData);
                eventGroupData = "";
            } */
        }
Esempio n. 3
0
        /// <summary> Parses the Replay.Messages.Events file. </summary>
        /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param>
        /// <returns> A list of messages parsed from the buffer. </returns>
        public static void Parse(Replay replay, byte[] buffer)
        {
            if (buffer.Length <= 1)
            {
                // Chat has been removed from this replay
                return;
            }

            var ticksElapsed = 0;

            using (var stream = new MemoryStream(buffer))
            {
                var bitReader = new Streams.BitReader(stream);

                while (!bitReader.EndOfStream)
                {
                    var message = new Message();

                    ticksElapsed     += (int)bitReader.Read(6 + (bitReader.Read(2) << 3));
                    message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(ticksElapsed / 16.0));

                    var playerIndex = (int)bitReader.Read(5);
                    if (playerIndex != 16)
                    {
                        message.MessageSender = replay.ClientListByUserID[playerIndex];
                    }

                    message.MessageEventType = (MessageEventType)bitReader.Read(4);
                    switch (message.MessageEventType)
                    {
                    case MessageEventType.SChatMessage:
                    {
                        ChatMessage chatMessage = new ChatMessage();

                        chatMessage.MessageTarget = (MessageTarget)bitReader.Read(3);                                  // m_recipient (the target)
                        chatMessage.Message       = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(11)); // m_string

                        message.ChatMessage = chatMessage;
                        replay.Messages.Add(message);
                        break;
                    }

                    case MessageEventType.SPingMessage:
                    {
                        PingMessage pingMessage = new PingMessage();

                        pingMessage.MessageTarget = (MessageTarget)bitReader.Read(3);         // m_recipient (the target)

                        pingMessage.XCoordinate = bitReader.ReadInt32() - (-2147483648);      // m_point x
                        pingMessage.YCoordinate = bitReader.ReadInt32() - (-2147483648);      // m_point y

                        message.PingMessage = pingMessage;
                        replay.Messages.Add(message);
                        break;
                    }

                    case MessageEventType.SLoadingProgressMessage:
                    {
                        // can be used to keep track of how fast/slow players are loading
                        // also includes players who are reloading the game
                        var progress = bitReader.ReadInt32() - (-2147483648);         // m_progress
                        break;
                    }

                    case MessageEventType.SServerPingMessage:
                    {
                        break;
                    }

                    case MessageEventType.SReconnectNotifyMessage:
                    {
                        bitReader.Read(2);         // m_status; is either a 1 or a 2
                        break;
                    }

                    case MessageEventType.SPlayerAnnounceMessage:
                    {
                        PlayerAnnounceMessage announceMessage = new PlayerAnnounceMessage();

                        announceMessage.AnnouncementType = (AnnouncementType)bitReader.Read(2);

                        switch (announceMessage.AnnouncementType)
                        {
                        case AnnouncementType.None:
                        {
                            break;
                        }

                        case AnnouncementType.Ability:
                        {
                            AbilityAnnouncment ability = new AbilityAnnouncment();
                            ability.AbilityLink  = bitReader.ReadInt16();               // m_abilLink
                            ability.AbilityIndex = (int)bitReader.Read(5);              // m_abilCmdIndex
                            ability.ButtonLink   = bitReader.ReadInt16();               // m_buttonLink

                            announceMessage.AbilityAnnouncement = ability;
                            break;
                        }

                        case AnnouncementType.Behavior:            // no idea what triggers this
                        {
                            bitReader.ReadInt16();                 // m_behaviorLink
                            bitReader.ReadInt16();                 // m_buttonLink
                            break;
                        }

                        case AnnouncementType.Vitals:
                        {
                            VitalAnnouncment vital = new VitalAnnouncment();
                            vital.VitalType = (VitalType)(bitReader.ReadInt16() - (-32768));

                            announceMessage.VitalAnnouncement = vital;
                            break;
                        }

                        default:
                            throw new NotImplementedException();
                        }

                        if (replay.ReplayBuild > 45635)
                        {
                            // m_announceLink
                            bitReader.ReadInt16();
                        }

                        bitReader.ReadInt32();         // m_otherUnitTag
                        bitReader.ReadInt32();         // m_unitTag

                        message.PlayerAnnounceMessage = announceMessage;
                        replay.Messages.Add(message);
                        break;
                    }

                    default:
                        throw new NotImplementedException();
                    }

                    bitReader.AlignToByte();
                }
            }
        }