saul / demofile-net

Blazing fast cross-platform demo parser library for Counter-Strike 2 and Valve's Deadlock, written in C#.
MIT License
111 stars 10 forks source link

Issue with detecting match start after recent updates. #123

Open xGuysOG opened 5 days ago

xGuysOG commented 5 days ago

Research

Description

This was brought up in https://github.com/saul/demofile-net/issues/84

i was told to use


demo.EntityEvents.CCSGameRulesProxy.AddChangeCallback(proxy => proxy.GameRules?.HasMatchStarted, (proxy, _, _) =>
            {

                hasMatchEverStarted |= demo.GameRules.HasMatchStarted;
            });

by @in0finite

This has worked for a while, but now with some new demos hasMatchEverStarted never becomes true

Code to reproduce

Below is the code i used, but all you really need is the hasMatchStarted stuff    
public static async Task Main(string[] args)
    {

        //return;
        try
        {
            //_demoBytes = File.ReadAllBytes(demoFilePath);
            await using var fileStream = File.OpenRead(flashWork);
            int currentRound = 0;
            int previousRoundTotal = 0;

            bool hasHandledData = false;
            bool hasMatchEverStarted = false;
            Dictionary<int, RoundEnd> roundEnds = new();
            Dictionary<ulong, List<PlayerStats>> playerStats = new();
            Dictionary<ulong, KillInfo> lastKiller = new();
            bool isFirstKillOfTheRound = true;
            bool isFirstDeathOfTheRound = true;
            var flashQueue = new List<CCSPlayerPawn>();
            var demo = new CsDemoParser();

            demo.EntityEvents.CCSGameRulesProxy.AddChangeCallback(proxy => proxy.GameRules?.HasMatchStarted, (proxy, _, _) =>
            {

                hasMatchEverStarted |= demo.GameRules.HasMatchStarted;
            });

            demo.Source1GameEvents.PlayerDeath += e =>
            {
                if (!hasMatchEverStarted)
                {
                    //PowerLigaManager.Log("Has match started is false, so we return player death");
                    return;
                }

                //PowerLigaManager.Log($"{e.Attacker?.PlayerName} [{e.Weapon}] {e.Player?.PlayerName}");
                if (e.Player == null)
                {
                    return;
                }
                UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.hasSurvived = false, demo, currentRound);
                if (e.Attacker == null && e.Player.Team == demo.TeamTerrorist)
                {
                    //PowerLigaManager.Log("So attacker is null, and this guy is terrorist reason is ");
                    //PowerLigaManager.Log("weapon is " + e.Weapon + " weaponid " + e.WeaponItemid + " weaponfaux " + e.WeaponFauxitemid);
                    if (e.Weapon == "planted_c4")
                    {
                        return;
                    }
                }

                UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.deaths++, demo, currentRound);
                if (isFirstDeathOfTheRound)
                {
                    UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.firstDeaths++, demo, currentRound);
                    isFirstDeathOfTheRound = false;
                }

                if (e.Attacker != null)
                {
                    if (e.Attacker.Team == e.Player.Team)
                    {
                        return;
                    }

                    if (e.Attacker.SteamID == e.Player.SteamID)
                    {
                        return;
                    }

                    if (isFirstKillOfTheRound)
                    {
                        UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.openingDuelAttempts++, demo, currentRound);
                        UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.openingDuelAttempts++, demo, currentRound);
                        UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.firstKills++, demo, currentRound);
                        isFirstKillOfTheRound = false;
                    }

                    UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.killedBy = e.Attacker.SteamID, demo, currentRound);

                    UpdateCS2PlayerStats(playerStats, e.Attacker, stat =>
                    {
                        stat.killAssistSurviveTrade++;
                        stat.kills++;
                        stat.killedPlayers.Add(e.Player.SteamID);  // Add the killed player to the list
                    }, demo, currentRound);
                    //PowerLigaManager.Log("Logging this kill. attacker is " + e.Attacker.PlayerName + " dead player is " + e.Player.PlayerName + " weapon was " + e.Weapon);
                    if (e.Weapon == "awp")
                    {
                        //PowerLigaManager.Log("Logging this confirmed kill. attacker is " + e.Attacker.PlayerName + " dead player is " + e.Player.PlayerName + " weapon was " + e.Weapon);
                        UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.awpKills++, demo, currentRound);
                    }
                    //PowerLigaManager.Log("Logging this kill. attacker is " + e.Attacker.PlayerName + " dead player is " + e.Player.PlayerName + " state is " + demo.GameRules.CSGamePhase + " current time: " + demo.CurrentGameTime + " startTime is " + demo.GameRules.GameStartTime);
                    UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.roundKills++, demo, currentRound);
                    if (e.Headshot)
                    {
                        UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.headShotKills++, demo, currentRound);
                    }

                    var victimSteamID = e.Player.SteamID;
                    var attackerSteamID = e.Attacker.SteamID;
                    //AnsiPowerLigaManager.Log("Okay so tickrate is ? " + parser.TickRate + " currenttick is " + parser.CurrentTick );
                    var currentTick = demo.CurrentGameTick.Value;
                    var previousKill = lastKiller.FirstOrDefault(kv => kv.Value.KillerSteamID == victimSteamID);
                    if (previousKill.Key != 0)
                    {

                        var previouslyKilledPlayerSteamID = previousKill.Key;
                        var previousKillInfo = previousKill.Value;
                        var tickDifference = currentTick - previousKillInfo.DeathTick;

                        // Check if it's within the trade kill timeframe
                        if (tickDifference <= TradeKillTimeframeTicksCS2)
                        {
                            // Check if the attacker is on the same team as the previously killed player
                            var previouslyKilledPlayer = demo.Players.FirstOrDefault(p => p.SteamID == previouslyKilledPlayerSteamID);
                            if (previouslyKilledPlayer != null)
                            {
                                var previouslyKilledPlayerTeam = previouslyKilledPlayer.CSTeamNum;
                                if (previouslyKilledPlayerTeam == e.Attacker.CSTeamNum)
                                {
                                    // This is a trade kill
                                    UpdateCS2PlayerStats(playerStats, e.Attacker, stat => stat.killAssistSurviveTrade++, demo, currentRound);
                                    //AnsiPowerLigaManager.Log($"{e.Attacker.PlayerName} traded by killing {e.Player.PlayerName}, who had previously killed {demo.Players.First(p => p.SteamID == previouslyKilledPlayerSteamID).PlayerName}");
                                }
                            }
                        }
                    }
                }

                // Update stats for assister, if assister exists
                if (e.Assister == null)
                {
                    return;
                }
                UpdateCS2PlayerStats(playerStats, e.Assister, stat => stat.killAssistSurviveTrade++, demo, currentRound);
                UpdateCS2PlayerStats(playerStats, e.Assister, stat => stat.assists++, demo, currentRound);

            };

            demo.Source1GameEvents.RoundStart += e =>
            {
                Console.WriteLine("Teamnames? CT: " + demo.TeamCounterTerrorist.ClanTeamname + " T: " + demo.TeamTerrorist.ClanTeamname);
                if (!hasMatchEverStarted)
                {
                    return;
                }
                Console.WriteLine("Teamnames? CT: " + demo.TeamCounterTerrorist.ClanTeamname + " T: " + demo.TeamTerrorist.ClanTeamname);

                int roundTotal = demo.TeamCounterTerrorist.Score + demo.TeamTerrorist.Score;
                isFirstDeathOfTheRound = true;
                isFirstKillOfTheRound = true;

                lastKiller.Clear();
                foreach (var player in demo.Players)
                {
                    UpdateCS2PlayerStats(playerStats, player, stat => stat.hasSurvived = true, demo, currentRound);
                }
                // Update current round based on the score
                if (roundTotal != previousRoundTotal)
                {
                    currentRound = roundTotal;
                    previousRoundTotal = roundTotal;
                }
                //Here we call the UpdatePlayerStatsWithMatchStats
                foreach (var player in demo.TeamCounterTerrorist.CSPlayerControllers)
                {
                    UpdatePlayerStatsWithMatchStats(playerStats, player.SteamID, player.ActionTrackingServices!.MatchStats, currentRound);
                }
                foreach (var player in demo.TeamTerrorist.CSPlayerControllers)
                {
                    UpdatePlayerStatsWithMatchStats(playerStats, player.SteamID, player.ActionTrackingServices!.MatchStats, currentRound);
                }
            };

            demo.EntityEvents.CCSPlayerPawn.AddChangeCallback(
    p => p.FlashDuration,
    (p, oldFlashDuration, newFlashDuration) =>
    {
        if (!hasMatchEverStarted)
        {
            //PowerLigaManager.Log("Has match started is false, so we return");
            return;
        }
        if (p == null || p.Team == null)
        {
            Console.WriteLine("Flash duration called but player or team is null.");
            return;
        }
        if (newFlashDuration > 0)
        {
            flashQueue.Add(p);
        }
    });

            demo.Source1GameEvents.FlashbangDetonate += e =>
            {
                //Console.WriteLine($"Flashbang detonated at {e.X},{e.Y},{e.Z} by {e.PlayerPawn}:");
                if (e.PlayerPawn == null)
                {
                    //PowerLigaManager.Log("Flashbang detonated but player is null.");
                    return;
                }
                if (e.Player == null)
                {
                    //PowerLigaManager.Log("Flashbang detonated but player is null.");
                    return;
                }
                foreach (var pawn in flashQueue)
                {
                    if (!hasMatchEverStarted)
                    {
                        //PowerLigaManager.Log("Has match started is false, so we return");
                        return;
                    }
                    if (pawn == null || pawn.Team == null)
                    {
                        //PowerLigaManager.Log("Flash duration called but player or team is null.");
                        continue;
                    }
                    if (pawn.Team == e.PlayerPawn.Team)
                    {
                        //PowerLigaManager.Log("Team flash ignore");
                        continue;
                    }

                    if (!(pawn.FlashDuration > 0)) continue;

                    if (pawn.Controller == null)
                    {
                        continue;
                    }
                    //Stopwatch stopwatch = new Stopwatch();
                    //stopwatch.Start();
                    UpdateCS2PlayerStats(playerStats, pawn.Controller, stat => stat.enemyFlashDuration += pawn.FlashDuration, demo, currentRound);
                    UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.enemiesFlashed++, demo, currentRound);
                    UpdateCS2PlayerStats(playerStats, e.Player, stat => stat.flashDuration += pawn.FlashDuration, demo, currentRound);
                    //CCSPlayerController
                    //pawn
                    //Console.WriteLine($"  - {pawn} - {pawn.FlashDuration} secs");
                }
                flashQueue.Clear();
            };

            demo.Source1GameEvents.RoundEnd += e =>
            {
                if (!hasMatchEverStarted)
                {
                    // PowerLigaManager.Log("ROund end called but we have an issue, it seems match was not started yet");
                    return;
                }
                CSRoundEndReason reasonEnum = (CSRoundEndReason)e.Reason;

                // Convert the enum value to its string representation
                string reasonString = reasonEnum.ToString();
                //detect who won
                CSTeamNumber teamwon = (CSTeamNumber)e.Winner;
                string? teamID = "";
                //TODO UNCOMMENT
                //if (teamwon == CSTeamNumber.CounterTerrorist)
                //{
                //    teamID = teamDA.GetTeamIdFromTeamNameAndSeasonSync(demo.TeamCounterTerrorist.ClanTeamname, season);
                //}
                //else if (teamwon == CSTeamNumber.Terrorist)
                //{
                //    teamID = teamDA.GetTeamIdFromTeamNameAndSeasonSync(demo.TeamTerrorist.ClanTeamname, season);
                //}
                int currentScore = demo.TeamCounterTerrorist.Score + demo.TeamTerrorist.Score;
                roundEnds[currentScore] = new RoundEnd
                {
                    Reason = reasonString,
                    TeamWonID = teamID
                };

                if (demo.TeamCounterTerrorist.Score + demo.TeamTerrorist.Score == 1 || demo.TeamCounterTerrorist.Score + demo.TeamTerrorist.Score == 13)
                {
                    var team1PlayerIDs = demo.TeamTerrorist.CSPlayerControllers.Select(player => player.SteamID.ToString()).ToList();
                    var team2PlayerIDs = demo.TeamCounterTerrorist.CSPlayerControllers.Select(player => player.SteamID.ToString()).ToList();

                    var winningTeam = e.Winner == 2 ? demo.TeamTerrorist : (e.Winner == 3 ? demo.TeamCounterTerrorist : null);

                    if (winningTeam != null)
                    {
                        var players = winningTeam.CSPlayerControllers.OrderByDescending(player => player.ActionTrackingServices!.MatchStats.Damage).ToArray();
                        foreach (var player in players)
                        {
                            UpdateCS2PlayerStats(playerStats, player, stat => stat.pistolRoundWins++, demo, currentScore);
                        }
                    }
                }

                //TODO Stopped here.

                foreach (var playerStatsList in playerStats.Values)
                {
                    // Filter for stats related to the current round
                    var currentRoundStats = playerStatsList.FirstOrDefault(stat => stat.roundnr == currentRound);
                    if (currentRoundStats == null)
                    {
                        continue; // Skip if no stats for this player in the current round
                    }

                    try
                    {
                        if (currentRoundStats.killAssistSurviveTrade > 0 || currentRoundStats.hasSurvived)
                        {
                            currentRoundStats.roundsWithImpact++;
                        }

                        if (currentRoundStats.roundKills > 0)
                        {
                            var index = Math.Min(currentRoundStats.roundKills, currentRoundStats.multiKills.Length - 1);
                            currentRoundStats.multiKills[index]++;
                        }

                        // Reset properties for the next round
                        currentRoundStats.hasSurvived = false;
                        currentRoundStats.killAssistSurviveTrade = 0;
                    }
                    catch (IndexOutOfRangeException ex)
                    {
                        //PowerLigaManager.Log($"IndexOutOfRangeException caught: {ex.Message}");
                        //PowerLigaManager.Log($"Stack Trace: {ex.StackTrace}");
                        //PowerLigaManager.Log($"Player roundKills: {player.roundKills}, multiKills Length: {player.multiKills.Length}");
                        // Optionally rethrow the exception if you want the application to crash
                        // throw;
                    }
                }
                //stopwatch.Stop();
                //PowerLigaManager.Log("RoundEnd timer is " + stopwatch.ElapsedMilliseconds);
            };

            demo.Source1GameEvents.CsWinPanelMatch += e =>
            {
                Console.WriteLine("WIn match has ended");

                var players = demo.TeamCounterTerrorist.CSPlayerControllers.OrderByDescending(player => player.ActionTrackingServices!.MatchStats.Damage).ToArray();
                var players2 = demo.TeamTerrorist.CSPlayerControllers.OrderByDescending(player => player.ActionTrackingServices!.MatchStats.Damage).ToArray();

                //loop above
                foreach (var player in players)
                {
                    Console.WriteLine("CT player " + player.PlayerName + " has " + player.ActionTrackingServices!.MatchStats.Damage + " damage");
                }

                foreach (var player in players2)
                {
                    Console.WriteLine("CT player " + player.PlayerName + " has " + player.ActionTrackingServices!.MatchStats.Damage + " damage");
                }
            };

            var reader = DemoFileReader.Create(demo, File.OpenRead(flashWork));
            await reader.ReadAllAsync();
            Console.WriteLine("\nFinished!");
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: " + e.Message.ToString());
        }

        Console.ReadKey();
    }

Affected demos

https://drive.google.com/drive/folders/1ZN0k7b-5zZk_6ls3JjHmzCwlA7DF65TZ?usp=sharing

in0finite commented 4 days ago

I just tested the provided demo in my demo viewer, and HasMatchStarted is always 1.

It could be that the property never changes, so AddChangeCallback() never invokes your callback. If that's the case, you can subscribe to demo.EntityEvents.CCSGameRulesProxy and assign initial value, something like this :

demo.EntityEvents.CCSGameRulesProxy.Create += e => hasMatchEverStarted |= e.GameRules.HasMatchStarted;
xGuysOG commented 2 days ago

I just tested the provided demo in my demo viewer, and HasMatchStarted is always 1.

It could be that the property never changes, so AddChangeCallback() never invokes your callback. If that's the case, you can subscribe to demo.EntityEvents.CCSGameRulesProxy and assign initial value, something like this :

demo.EntityEvents.CCSGameRulesProxy.Create += e => hasMatchEverStarted |= e.GameRules.HasMatchStarted;

Interresting, that does work on the new demos, but not on old ones.

There has to be a better way to do this, so i dont have to check for the version of the file and then run the code based on that right?

OLD demo i used to test: https://drive.google.com/file/d/16FwrcxRwHCUOR3v8weZKufAQWY7r7YgQ/view?usp=sharing

in0finite commented 2 days ago

I meant that you should use both approaches