Example #1
0
        public IntPtr _object; //Assume this always has largest offset
        public PlayerInfo(IntPtr baseAddr, ProcessMemory MemInstance, GameOffsets CurrentOffsets)
        {
            unsafe {
                var    baseAddrCopy         = baseAddr;
                int    last                 = MemInstance.OffsetAddress(ref baseAddrCopy, 0, 0);
                var    intPtrSize           = MemInstance.is64Bit ? 8 : 4;
                int    size                 = ((int)Math.Ceiling((decimal)((intPtrSize + CurrentOffsets.PlayerInfoStructOffsets.ObjectOffset) / 8))) * 8; //Find the nearest multiple of 8
                byte[] buffer               = MemInstance.Read(baseAddrCopy + last, size);
                PlayerInfoStructOffsets pOf = CurrentOffsets.PlayerInfoStructOffsets;
                fixed(byte *ptr = buffer)
                {
                    var buffptr = (IntPtr)ptr;

                    PlayerId = Marshal.ReadByte(buffptr, pOf.PlayerIDOffset);
                    var NamePTR = MemInstance.is64Bit ? (IntPtr)Marshal.ReadInt64(buffptr, pOf.PlayerNameOffset) : (IntPtr)Marshal.ReadInt32(buffptr, pOf.PlayerNameOffset);

                    PlayerName   = NamePTR == IntPtr.Zero ? "" : MemInstance.ReadString(NamePTR, CurrentOffsets.StringOffsets[0], CurrentOffsets.StringOffsets[1]);
                    ColorId      = (PlayerColor)(uint)Marshal.ReadInt32(buffptr, pOf.ColorIDOffset);
                    HatId        = (uint)Marshal.ReadInt32(buffptr, pOf.HatIDOffset);
                    PetId        = (uint)Marshal.ReadInt32(buffptr, pOf.PetIDOffset);
                    SkinId       = (uint)Marshal.ReadInt32(buffptr, pOf.SkinIDOffset);
                    Disconnected = Marshal.ReadByte(buffptr, pOf.DisconnectedOffset) > 0;
                    Tasks        = Marshal.ReadIntPtr(buffptr, pOf.TasksOffset);
                    IsImpostor   = Marshal.ReadByte(buffptr, pOf.ImposterOffset) == 1;
                    IsDead       = Marshal.ReadByte(buffptr, pOf.DeadOffset) > 0;
                    _object      = Marshal.ReadIntPtr(buffptr, pOf.ObjectOffset);
                }
            }
        }
Example #2
0
        public IntPtr _object; //Assume this always has largest offset

        public PlayerInfo(IntPtr baseAddr, ProcessMemory MemInstance, GameOffsets CurrentOffsets)
        {
            unsafe {
                var    baseAddrCopy           = baseAddr;
                int    last                   = MemInstance.OffsetAddress(ref baseAddrCopy, 0, 0);
                var    intPtrSize             = MemInstance.is64Bit ? 8 : 4;
                int    size                   = ((int)Math.Ceiling((decimal)((intPtrSize + CurrentOffsets.PlayerInfoStructOffsets.ObjectOffset) / 8))) * 8; //Find the nearest multiple of 8
                byte[] buffer                 = MemInstance.Read(baseAddrCopy + last, size);
                PlayerInfoStructOffsets   pOf = CurrentOffsets.PlayerInfoStructOffsets;
                PlayerOutfitStructOffsets oOf = CurrentOffsets.PlayerOutfitStructOffsets;
                var outfit = MemInstance.Read <IntPtr>(baseAddrCopy, pOf.OutfitsOffset);
                fixed(byte *ptr = buffer)
                {
                    var buffptr = (IntPtr)ptr;

                    PlayerId     = Marshal.ReadByte(buffptr, pOf.PlayerIDOffset);
                    Disconnected = Marshal.ReadByte(buffptr, pOf.DisconnectedOffset) > 0;
                    Tasks        = Marshal.ReadIntPtr(buffptr, pOf.TasksOffset);
                    IsDead       = Marshal.ReadByte(buffptr, pOf.IsDeadOffset) > 0;
                    _object      = Marshal.ReadIntPtr(buffptr, pOf.ObjectOffset);

                    // Read from Role
                    RoleType     = (uint)MemInstance.Read <int>(baseAddrCopy, pOf.RoleTypeOffset);
                    RoleTeamType = (uint)MemInstance.Read <int>(baseAddrCopy, pOf.RoleTeamTypeOffset);
                    IsImpostor   = RoleTeamType == 1;

                    // Read from PlayerOutfit
                    PlayerName = MemInstance.ReadString(MemInstance.Read <IntPtr>(outfit, oOf.PlayerNameOffset), CurrentOffsets.StringOffsets[0], CurrentOffsets.StringOffsets[1]);
                    ColorId    = (PlayerColor)(uint)MemInstance.Read <int>(outfit, oOf.ColorIDOffset);
                    // TODO: Since IDs are changed from enum to string like "hat_police", renaming or mapping existing svgs to string is required
                    // TODO: As a workaround just fill with 0 as IDs
                    //HatId = MemInstance.ReadString(MemInstance.Read<IntPtr>(outfit, oOf.HatIDOffset), CurrentOffsets.StringOffsets[0], CurrentOffsets.StringOffsets[1]);
                    //PetId = MemInstance.ReadString(MemInstance.Read<IntPtr>(outfit, oOf.PetIDOffset), CurrentOffsets.StringOffsets[0], CurrentOffsets.StringOffsets[1]);
                    //SkinId = MemInstance.ReadString(MemInstance.Read<IntPtr>(outfit, oOf.SkinIDOffset), CurrentOffsets.StringOffsets[0], CurrentOffsets.StringOffsets[1]);
                    HatId  = 0;
                    PetId  = 0;
                    SkinId = 0;
                }
            }
        }
Example #3
0
 public string GetPlayerName()
 {
     return(ProcessMemory.ReadString((IntPtr)this.PlayerName));
 }
Example #4
0
        public void RunLoop()
        {
            while (true)
            {
                if (!ProcessMemory.IsHooked)
                {
                    if (!ProcessMemory.HookProcess("Among Us"))
                    {
                        Thread.Sleep(1000);
                        continue;
                    }
                    else
                    {
                        Program.conInterface.WriteLine($"Connected to Among Us process ({ProcessMemory.process.Id})");

                        bool foundModule = false;

                        while (true)
                        {
                            foreach (ProcessMemory.Module module in ProcessMemory.modules)
                            {
                                if (module.Name.Equals("GameAssembly.dll", StringComparison.OrdinalIgnoreCase))
                                {
                                    GameAssemblyPtr = module.BaseAddress;
                                    foundModule     = true;
                                    break;
                                }
                            }

                            if (!foundModule)
                            {
                                Program.conInterface.WriteLine("Still looking for modules..."); // TODO: This still isn't functional, we need to re-hook to reload module addresses
                                Thread.Sleep(500);                                              // delay and try again
                            }
                            else
                            {
                                break; // we have found all modules
                            }
                        }


                        Console.WriteLine($"({GameAssemblyPtr})");
                        prevChatBubsVersion = ProcessMemory.Read <int>(GameAssemblyPtr, HudManagerOffset, 0x5C, 0, 0x28, 0xC, 0x14, 0x10);
                    }
                }

                GameState state;
                //int meetingHudState = /*meetingHud_cachePtr == 0 ? 4 : */ProcessMemory.ReadWithDefault<int>(GameAssemblyPtr, 4, 0xDA58D0, 0x5C, 0, 0x84); // 0 = Discussion, 1 = NotVoted, 2 = Voted, 3 = Results, 4 = Proceeding
                IntPtr meetingHud          = ProcessMemory.Read <IntPtr>(GameAssemblyPtr, MeetingHudOffset, 0x5C, 0);
                uint   meetingHud_cachePtr = meetingHud == IntPtr.Zero ? 0 : ProcessMemory.Read <uint>(meetingHud, 0x8);
                int    meetingHudState     = meetingHud_cachePtr == 0 ? 4 : ProcessMemory.ReadWithDefault <int>(meetingHud, 4, 0x84); // 0 = Discussion, 1 = NotVoted, 2 = Voted, 3 = Results, 4 = Proceeding
                int    gameState           = ProcessMemory.Read <int>(GameAssemblyPtr, AmongUsClientOffset, 0x5C, 0, 0x64);           // 0 = NotJoined, 1 = Joined, 2 = Started, 3 = Ended (during "defeat" or "victory" screen only)

                if (gameState == 0)
                {
                    state          = GameState.MENU;
                    exileCausesEnd = false;
                }
                else if (gameState == 1 || gameState == 3)
                {
                    state          = GameState.LOBBY;
                    exileCausesEnd = false;
                }
                else if (exileCausesEnd)
                {
                    state = GameState.LOBBY;
                }
                else if (meetingHudState < 4)
                {
                    state = GameState.DISCUSSION;
                }
                else
                {
                    state = GameState.TASKS;
                }

                IntPtr allPlayersPtr = ProcessMemory.Read <IntPtr>(GameAssemblyPtr, GameDataOffset, 0x5C, 0, 0x24);
                IntPtr allPlayers    = ProcessMemory.Read <IntPtr>(allPlayersPtr, 0x08);
                int    playerCount   = ProcessMemory.Read <int>(allPlayersPtr, 0x0C);

                IntPtr playerAddrPtr = allPlayers + 0x10;

                // check if exile causes end
                if (oldState == GameState.DISCUSSION && state == GameState.TASKS)
                {
                    byte exiledPlayerId = ProcessMemory.ReadWithDefault <byte>(GameAssemblyPtr, 255, MeetingHudOffset, 0x5C, 0, 0x94, 0x08);
                    int  impostorCount = 0, innocentCount = 0;

                    for (int i = 0; i < playerCount; i++)
                    {
                        PlayerInfo pi = ProcessMemory.Read <PlayerInfo>(playerAddrPtr, 0, 0);
                        playerAddrPtr += 4;

                        if (pi.PlayerId == exiledPlayerId)
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                            {
                                Action       = PlayerAction.Exiled,
                                Name         = pi.GetPlayerName(),
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        // skip invalid, dead and exiled players
                        if (pi.PlayerName == 0 || pi.PlayerId == exiledPlayerId || pi.IsDead == 1 || pi.Disconnected == 1)
                        {
                            continue;
                        }

                        if (pi.IsImpostor == 1)
                        {
                            impostorCount++;
                        }
                        else
                        {
                            innocentCount++;
                        }
                    }

                    if (impostorCount == 0 || impostorCount >= innocentCount)
                    {
                        exileCausesEnd = true;
                        state          = GameState.LOBBY;
                    }
                }

                if (this.shouldTransmitState)
                {
                    shouldTransmitState = false;
                    GameStateChanged?.Invoke(this, new GameStateChangedEventArgs()
                    {
                        NewState = state
                    });
                }
                else if (state != oldState)
                {
                    GameStateChanged?.Invoke(this, new GameStateChangedEventArgs()
                    {
                        NewState = state
                    });
                }

                oldState = state;

                newPlayerInfos.Clear();

                playerAddrPtr = allPlayers + 0x10;

                for (int i = 0; i < playerCount; i++)
                {
                    PlayerInfo pi = ProcessMemory.Read <PlayerInfo>(playerAddrPtr, 0, 0);
                    playerAddrPtr += 4;
                    if (pi.PlayerName == 0)
                    {
                        continue;
                    }
                    string playerName = pi.GetPlayerName();

                    newPlayerInfos[playerName] = pi;             // add to new playerinfos for comparison later

                    if (!oldPlayerInfos.ContainsKey(playerName)) // player wasn't here before, they just joined
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                        {
                            Action       = PlayerAction.Joined,
                            Name         = playerName,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                    else     // player was here before, we have an old playerInfo to compare against
                    {
                        PlayerInfo oldPlayerInfo = oldPlayerInfos[playerName];
                        if (!oldPlayerInfo.GetIsDead() && pi.GetIsDead()) // player just died
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                            {
                                Action       = PlayerAction.Died,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        if (oldPlayerInfo.ColorId != pi.ColorId)
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                            {
                                Action       = PlayerAction.ChangedColor,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        if (!oldPlayerInfo.GetIsDisconnected() && pi.GetIsDisconnected())
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                            {
                                Action       = PlayerAction.Disconnected,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }
                    }
                }

                foreach (KeyValuePair <string, PlayerInfo> kvp in oldPlayerInfos)
                {
                    PlayerInfo pi         = kvp.Value;
                    string     playerName = kvp.Key;
                    if (!newPlayerInfos.ContainsKey(playerName)) // player was here before, isn't now, so they left
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                        {
                            Action       = PlayerAction.Left,
                            Name         = playerName,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                }

                oldPlayerInfos.Clear();

                bool emitAll = false;
                if (shouldForceUpdate)
                {
                    shouldForceUpdate = false;
                    emitAll           = true;
                }

                foreach (KeyValuePair <string, PlayerInfo> kvp in newPlayerInfos) // do this instead of assignment so they don't point to the same object
                {
                    PlayerInfo pi = kvp.Value;
                    oldPlayerInfos[kvp.Key] = pi;
                    if (emitAll)
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs()
                        {
                            Action       = PlayerAction.ForceUpdated,
                            Name         = kvp.Key,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                }

                IntPtr chatBubblesPtr = ProcessMemory.Read <IntPtr>(GameAssemblyPtr, HudManagerOffset, 0x5C, 0, 0x28, 0xC, 0x14);

                int poolSize = 20; // = ProcessMemory.Read<int>(GameAssemblyPtr, 0xD0B25C, 0x5C, 0, 0x28, 0xC, 0xC)

                int      numChatBubbles  = ProcessMemory.Read <int>(chatBubblesPtr, 0xC);
                int      chatBubsVersion = ProcessMemory.Read <int>(chatBubblesPtr, 0x10);
                IntPtr   chatBubblesAddr = ProcessMemory.Read <IntPtr>(chatBubblesPtr, 0x8) + 0x10;
                IntPtr[] chatBubblePtrs  = ProcessMemory.ReadArray(chatBubblesAddr, numChatBubbles);

                int newMsgs = 0;

                if (chatBubsVersion > prevChatBubsVersion) // new message has been sent
                {
                    if (chatBubsVersion > poolSize)        // increments are twofold (push to and pop from pool)
                    {
                        if (prevChatBubsVersion > poolSize)
                        {
                            newMsgs = (chatBubsVersion - prevChatBubsVersion) >> 1;
                        }
                        else
                        {
                            newMsgs = (poolSize - prevChatBubsVersion) + ((chatBubsVersion - poolSize) >> 1);
                        }
                    }
                    else // single increments
                    {
                        newMsgs = chatBubsVersion - prevChatBubsVersion;
                    }
                }
                else if (chatBubsVersion < prevChatBubsVersion) // reset
                {
                    if (chatBubsVersion > poolSize)             // increments are twofold (push to and pop from pool)
                    {
                        newMsgs = poolSize + ((chatBubsVersion - poolSize) >> 1);
                    }
                    else // single increments
                    {
                        newMsgs = chatBubsVersion;
                    }
                }

                prevChatBubsVersion = chatBubsVersion;

                for (int i = numChatBubbles - newMsgs; i < numChatBubbles; i++)
                {
                    string msgText = ProcessMemory.ReadString(ProcessMemory.Read <IntPtr>(chatBubblePtrs[i], 0x20, 0x28));
                    if (msgText.Length == 0)
                    {
                        continue;
                    }
                    string msgSender = ProcessMemory.ReadString(ProcessMemory.Read <IntPtr>(chatBubblePtrs[i], 0x1C, 0x28));
                    ChatMessageAdded?.Invoke(this, new ChatMessageEventArgs()
                    {
                        Sender  = msgSender,
                        Message = msgText
                    });
                }

                //string gameCode = ProcessMemory.ReadString(ProcessMemory.Read<IntPtr>(GameAssemblyPtr, GameStartManagerOffset, 0x5c, 0, 0x20, 0x28));
                //Console.WriteLine(gameCode);

                Thread.Sleep(250);
            }
        }
        public void RunLoop()
        {
            while (true)
            {
                if (!ProcessMemory.IsHooked)
                {
                    if (!ProcessMemory.HookProcess("Among Us"))
                    {
                        Thread.Sleep(1000);
                        continue;
                    }

                    Settings.conInterface.WriteModuleTextColored("GameMemReader", Color.Lime,
                                                                 $"Connected to Among Us process ({Color.Red.ToTextColor()}{ProcessMemory.process.Id}{MainWindow.NormalTextColor.ToTextColor()})");


                    var foundModule = false;

                    while (true)
                    {
                        foreach (var module in ProcessMemory.modules)
                        {
                            if (module.Name.Equals("GameAssembly.dll", StringComparison.OrdinalIgnoreCase))
                            {
                                GameAssemblyPtr = module.BaseAddress;
                                if (!GameVerifier.VerifySteamHash(module.FileName))
                                {
                                    cracked = true;
                                    Settings.conInterface.WriteModuleTextColored("GameVerifier", Color.Red,
                                                                                 $"Client verification: {Color.Red.ToTextColor()}FAIL{MainWindow.NormalTextColor.ToTextColor()}.");
                                }
                                else
                                {
                                    cracked = false;
                                    Settings.conInterface.WriteModuleTextColored("GameVerifier", Color.Red,
                                                                                 $"Client verification: {Color.Lime.ToTextColor()}PASS{MainWindow.NormalTextColor.ToTextColor()}.");
                                }

                                foundModule = true;
                                break;
                            }
                        }

                        if (!foundModule)
                        {
                            Settings.conInterface.WriteModuleTextColored("GameMemReader", Color.Lime,
                                                                         "Still looking for modules...");
                            //Program.conInterface.WriteModuleTextColored("GameMemReader", Color.Green, "Still looking for modules..."); // TODO: This still isn't functional, we need to re-hook to reload module addresses
                            Thread.Sleep(500); // delay and try again
                            ProcessMemory.LoadModules();
                        }
                        else
                        {
                            break; // we have found all modules
                        }
                    }

                    prevChatBubsVersion = ProcessMemory.Read <int>(GameAssemblyPtr, _gameOffsets.HudManagerOffset, 0x5C,
                                                                   0, 0x28, 0xC, 0x14, 0x10);
                }
                if (cracked && ProcessMemory.IsHooked)
                {
                    Settings.form.PlayGotEm();
                    var result = Settings.form.context.DialogCoordinator.ShowMessageAsync(Settings.form.context,
                                                                                          "Uh oh.",
                                                                                          "We have detected that you are running an unsupported version of the game. This may or may not work.",
                                                                                          MessageDialogStyle.AffirmativeAndNegative,
                                                                                          new MetroDialogSettings
                    {
                        AffirmativeButtonText = "I understand", NegativeButtonText = "Exit",
                        ColorScheme           = MetroDialogColorScheme.Theme,
                        DefaultButtonFocus    = MessageDialogResult.Negative
                    })
                                 .GetAwaiter().GetResult();
                    if (result == MessageDialogResult.Negative)
                    {
                        Environment.Exit(0);
                    }
                    else
                    {
                        cracked = false;
                    }
                    continue;
                }

                GameState state;
                //int meetingHudState = /*meetingHud_cachePtr == 0 ? 4 : */ProcessMemory.ReadWithDefault<int>(GameAssemblyPtr, 4, 0xDA58D0, 0x5C, 0, 0x84); // 0 = Discussion, 1 = NotVoted, 2 = Voted, 3 = Results, 4 = Proceeding
                var meetingHud          = ProcessMemory.Read <IntPtr>(GameAssemblyPtr, _gameOffsets.MeetingHudOffset, 0x5C, 0);
                var meetingHud_cachePtr = meetingHud == IntPtr.Zero ? 0 : ProcessMemory.Read <uint>(meetingHud, 0x8);
                var meetingHudState     =
                    meetingHud_cachePtr == 0
                        ? 4
                        : ProcessMemory.ReadWithDefault(meetingHud, 4,
                                                        0x84); // 0 = Discussion, 1 = NotVoted, 2 = Voted, 3 = Results, 4 = Proceeding
                var gameState =
                    ProcessMemory.Read <int>(GameAssemblyPtr, _gameOffsets.AmongUsClientOffset, 0x5C, 0,
                                             0x64); // 0 = NotJoined, 1 = Joined, 2 = Started, 3 = Ended (during "defeat" or "victory" screen only)

                switch (gameState)
                {
                case 0:
                    state          = GameState.MENU;
                    exileCausesEnd = false;
                    break;

                case 1:
                case 3:
                    state          = GameState.LOBBY;
                    exileCausesEnd = false;
                    break;

                default:
                {
                    if (exileCausesEnd)
                    {
                        state = GameState.LOBBY;
                    }
                    else if (meetingHudState < 4)
                    {
                        state = GameState.DISCUSSION;
                    }
                    else
                    {
                        state = GameState.TASKS;
                    }

                    break;
                }
                }


                var allPlayersPtr =
                    ProcessMemory.Read <IntPtr>(GameAssemblyPtr, _gameOffsets.GameDataOffset, 0x5C, 0, 0x24);
                var allPlayers  = ProcessMemory.Read <IntPtr>(allPlayersPtr, 0x08);
                var playerCount = ProcessMemory.Read <int>(allPlayersPtr, 0x0C);

                var playerAddrPtr = allPlayers + 0x10;

                // check if exile causes end
                if (oldState == GameState.DISCUSSION && state == GameState.TASKS)
                {
                    var exiledPlayerId = ProcessMemory.ReadWithDefault <byte>(GameAssemblyPtr, 255,
                                                                              _gameOffsets.MeetingHudOffset, 0x5C, 0, 0x94, 0x08);
                    int impostorCount = 0, innocentCount = 0;

                    for (var i = 0; i < playerCount; i++)
                    {
                        var pi = ProcessMemory.Read <PlayerInfo>(playerAddrPtr, 0, 0);
                        playerAddrPtr += 4;

                        if (pi.PlayerId == exiledPlayerId)
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                            {
                                Action       = PlayerAction.Exiled,
                                Name         = pi.GetPlayerName(),
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        // skip invalid, dead and exiled players
                        if (pi.PlayerName == 0 || pi.PlayerId == exiledPlayerId || pi.IsDead == 1 ||
                            pi.Disconnected == 1)
                        {
                            continue;
                        }

                        if (pi.IsImpostor == 1)
                        {
                            impostorCount++;
                        }
                        else
                        {
                            innocentCount++;
                        }
                    }

                    if (impostorCount == 0 || impostorCount >= innocentCount)
                    {
                        exileCausesEnd = true;
                        state          = GameState.LOBBY;
                    }
                }

                if (state != oldState || shouldForceTransmitState)
                {
                    GameStateChanged?.Invoke(this, new GameStateChangedEventArgs {
                        NewState = state
                    });
                    shouldForceTransmitState = false;
                }

                if (state != oldState && state == GameState.LOBBY)
                {
                    shouldReadLobby = true; // will eventually transmit
                }

                oldState = state;

                newPlayerInfos.Clear();

                playerAddrPtr = allPlayers + 0x10;

                for (var i = 0; i < playerCount; i++)
                {
                    var pi = ProcessMemory.Read <PlayerInfo>(playerAddrPtr, 0, 0);
                    playerAddrPtr += 4;
                    if (pi.PlayerName == 0)
                    {
                        continue;
                    }
                    var playerName = pi.GetPlayerName();
                    if (playerName.Length == 0)
                    {
                        continue;
                    }

                    newPlayerInfos[playerName] = pi;             // add to new playerinfos for comparison later

                    if (!oldPlayerInfos.ContainsKey(playerName)) // player wasn't here before, they just joined
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                        {
                            Action       = PlayerAction.Joined,
                            Name         = playerName,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                    else
                    {
                        // player was here before, we have an old playerInfo to compare against
                        var oldPlayerInfo = oldPlayerInfos[playerName];
                        if (!oldPlayerInfo.GetIsDead() && pi.GetIsDead()) // player just died
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                            {
                                Action       = PlayerAction.Died,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        if (oldPlayerInfo.ColorId != pi.ColorId)
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                            {
                                Action       = PlayerAction.ChangedColor,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }

                        if (!oldPlayerInfo.GetIsDisconnected() && pi.GetIsDisconnected())
                        {
                            PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                            {
                                Action       = PlayerAction.Disconnected,
                                Name         = playerName,
                                IsDead       = pi.GetIsDead(),
                                Disconnected = pi.GetIsDisconnected(),
                                Color        = pi.GetPlayerColor()
                            });
                        }
                    }
                }

                foreach (var kvp in oldPlayerInfos)
                {
                    var pi         = kvp.Value;
                    var playerName = kvp.Key;
                    if (!newPlayerInfos.ContainsKey(playerName)) // player was here before, isn't now, so they left
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                        {
                            Action       = PlayerAction.Left,
                            Name         = playerName,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                }

                oldPlayerInfos.Clear();

                var emitAll = false;
                if (shouldForceUpdatePlayers)
                {
                    shouldForceUpdatePlayers = false;
                    emitAll = true;
                }

                foreach (var kvp in newPlayerInfos
                         ) // do this instead of assignment so they don't point to the same object
                {
                    var pi = kvp.Value;
                    oldPlayerInfos[kvp.Key] = pi;
                    if (emitAll)
                    {
                        PlayerChanged?.Invoke(this, new PlayerChangedEventArgs
                        {
                            Action       = PlayerAction.ForceUpdated,
                            Name         = kvp.Key,
                            IsDead       = pi.GetIsDead(),
                            Disconnected = pi.GetIsDisconnected(),
                            Color        = pi.GetPlayerColor()
                        });
                    }
                }

                var chatBubblesPtr = ProcessMemory.Read <IntPtr>(GameAssemblyPtr, _gameOffsets.HudManagerOffset, 0x5C, 0,
                                                                 0x28, 0xC, 0x14);

                var poolSize = 20; // = ProcessMemory.Read<int>(GameAssemblyPtr, 0xD0B25C, 0x5C, 0, 0x28, 0xC, 0xC)

                var numChatBubbles  = ProcessMemory.Read <int>(chatBubblesPtr, 0xC);
                var chatBubsVersion = ProcessMemory.Read <int>(chatBubblesPtr, 0x10);
                var chatBubblesAddr = ProcessMemory.Read <IntPtr>(chatBubblesPtr, 0x8) + 0x10;
                var chatBubblePtrs  = ProcessMemory.ReadArray(chatBubblesAddr, numChatBubbles);

                var newMsgs = 0;

                if (chatBubsVersion > prevChatBubsVersion) // new message has been sent
                {
                    if (chatBubsVersion > poolSize)        // increments are twofold (push to and pop from pool)
                    {
                        if (prevChatBubsVersion > poolSize)
                        {
                            newMsgs = (chatBubsVersion - prevChatBubsVersion) >> 1;
                        }
                        else
                        {
                            newMsgs = poolSize - prevChatBubsVersion + ((chatBubsVersion - poolSize) >> 1);
                        }
                    }
                    else // single increments
                    {
                        newMsgs = chatBubsVersion - prevChatBubsVersion;
                    }
                }
                else if (chatBubsVersion < prevChatBubsVersion) // reset
                {
                    if (chatBubsVersion > poolSize)             // increments are twofold (push to and pop from pool)
                    {
                        newMsgs = poolSize + ((chatBubsVersion - poolSize) >> 1);
                    }
                    else // single increments
                    {
                        newMsgs = chatBubsVersion;
                    }
                }

                prevChatBubsVersion = chatBubsVersion;

                for (var i = numChatBubbles - newMsgs; i < numChatBubbles; i++)
                {
                    var msgText = ProcessMemory.ReadString(ProcessMemory.Read <IntPtr>(chatBubblePtrs[i], 0x20, 0x28));
                    if (msgText.Length == 0)
                    {
                        continue;
                    }
                    var msgSender     = ProcessMemory.ReadString(ProcessMemory.Read <IntPtr>(chatBubblePtrs[i], 0x1C, 0x28));
                    var oldPlayerInfo = oldPlayerInfos[msgSender];
                    ChatMessageAdded?.Invoke(this, new ChatMessageEventArgs
                    {
                        Sender  = msgSender,
                        Message = msgText,
                        Color   = oldPlayerInfo.GetPlayerColor()
                    });
                }

                if (shouldReadLobby)
                {
                    var gameCode = ProcessMemory.ReadString(ProcessMemory.Read <IntPtr>(GameAssemblyPtr,
                                                                                        _gameOffsets.GameStartManagerOffset, 0x5c, 0, 0x20, 0x28));
                    string[] split;
                    if (gameCode != null && gameCode.Length > 0 && (split = gameCode.Split('\n')).Length == 2)
                    {
                        PlayRegion region = (PlayRegion)((4 - (ProcessMemory.Read <int>(GameAssemblyPtr, _gameOffsets.ServerManagerOffset, 0x5c, 0, 0x10, 0x8, 0x8) & 0b11)) % 3); // do NOT ask

                        this.latestLobbyEventArgs = new LobbyEventArgs()
                        {
                            LobbyCode = split[1],
                            Region    = region
                        };
                        shouldReadLobby     = false;
                        shouldTransmitLobby = true; // since this is probably new info
                    }
                }

                if (shouldTransmitLobby)
                {
                    if (this.latestLobbyEventArgs != null)
                    {
                        JoinedLobby?.Invoke(this, this.latestLobbyEventArgs);
                    }
                    shouldTransmitLobby = false;
                }

                Thread.Sleep(250);
            }
        }