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

Round Start and Round End events not called in new update on new demos. #41

Closed xGuysOG closed 9 months ago

xGuysOG commented 9 months ago

Research

Description

Title really says all, but here is a demo if you need it:

Code example, player death event will run as normal but no RoundStart, RoundEnd or GameStart will be called, events that is rather needed to display round changes etc.

Code to reproduce

public static async Task Main(string[] args)
    {
        var demo = new DemoParser();
        demo.Source1GameEvents.PlayerDeath += e =>
        {
            Console.WriteLine($"{e.Attacker?.PlayerName} [{e.Weapon}] {e.Player?.PlayerName}");
        };

        demo.Source1GameEvents.RoundStart += e =>
        {
            Console.WriteLine($"Round start");
        };
        demo.Source1GameEvents.GameStart += e =>
        {
            Console.WriteLine($"Game start");
        };
        demo.Source1GameEvents.RoundEnd += e =>
        {
            Console.WriteLine($"Round end");
        };

        await demo.ReadAllAsync(File.OpenRead("D:\\Testing\\Sæson 25\\3divC\\goatgang_vs_UnsignedPuggers_map1_de_ancient_2024-02-07_20-01-15_6599f0ecca2465f94caa2467_1707336075213\\6599f0ecca2465f94caa2467_0_1707336075213.dem"));

        Console.WriteLine("\nFinished!");
    }

Affected demos

https://we.tl/t-3WP54hnNAc Apologizes for 2 different link types, i realized google made alot more sense after uploading the first one 😓 https://drive.google.com/file/d/1QCmjuZsS2QkglWWbccO77I2Ag6R5bJJ8/view?usp=sharing

saul commented 9 months ago

I believe since the 6th Feb update the round_start/end events are no longer fired. Instead you can track it via the GameRulesProxy entity, something similar to this:

demo.EntityEvents.CCSGameRulesProxy.AddChangeCallback(proxy => proxy.GameRules?.RoundEndCount, (proxy, _, _) =>
{
    var roundEndReason = (CSRoundEndReason) proxy.GameRules!.RoundEndReason;
    var winningTeam = (CSTeamNumber) proxy.GameRules!.RoundEndWinnerTeam switch
    {
        CSTeamNumber.Terrorist => demo.TeamTerrorist,
        CSTeamNumber.CounterTerrorist => demo.TeamCounterTerrorist,
        _ => null
    };

    Console.WriteLine($"{roundEndReason} won by {winningTeam}");
});
xGuysOG commented 9 months ago

@saul I see, this does work but introduces an issue where round end will be called before the last player death event :/

xGuysOG commented 9 months ago

@saul I see, this does work but introduces an issue where round end will be called before the last player death event :/

CSwin screen could just be used instead, but it causes the user of the api to manually keep track of versions which seems rather wasteful IMO. as the new solution you gave me dosnt work on the older demos, there i would need the old endEvents @saul

in0finite commented 9 months ago

I agree that library should provide high-level API access to basic events such as RoundStart/RoundEnd. It could, for example, simulate these events (invoke them manually) on newer demos.

As for portable solution, for now you can use CCSGameRules.CSRoundResults to get round results, and then keep track of it between updates, so you can detect changes.

To detect it AFTER player death, simply check for it after calling ReadNextAsync() - I think it should work.

saul commented 9 months ago

As for a portable solution, you can simply listen to round_end and the game rules var changing. The var doesn't exist in old demos, and the event doesn't exist in new demos, so it's portable.

void OnRoundEnd(DemoParser demo, CSRoundEndReason roundEndReason, CSTeamNumber winningTeamNumber)
{
    var winningTeam = winningTeamNumber switch
    {
        CSTeamNumber.Terrorist => demo.TeamTerrorist,
        CSTeamNumber.CounterTerrorist => demo.TeamCounterTerrorist,
        _ => null
    };

    Console.WriteLine($"\n>>> Round end: {roundEndReason}");
    Console.WriteLine($"  Winner: ({winningTeam?.Teamname}) {winningTeam?.ClanTeamname}");
    Console.WriteLine($"  {demo.GameRules.RoundsPlayedThisPhase} rounds played in {demo.GameRules.CSGamePhase}");
    Console.WriteLine($"  Scores: {demo.TeamTerrorist.ClanTeamname} {demo.TeamTerrorist.Score} - {demo.TeamCounterTerrorist.Score} {demo.TeamCounterTerrorist.ClanTeamname}");
}

demo.EntityEvents.CCSGameRulesProxy.AddChangeCallback(proxy => proxy.GameRules?.RoundEndCount, (proxy, _, _) =>
{
    var roundEndReason = (CSRoundEndReason) proxy.GameRules!.RoundEndReason;
    var winningTeamNumber = (CSTeamNumber) proxy.GameRules!.RoundEndWinnerTeam;

    // Entity updates happen mid-tick.
    // Wait until the start of the next tick to ensure any player deaths have happened.
    demo.CreateTimer(default(GameTick), () =>
    {
        OnRoundEnd(demo, roundEndReason, winningTeamNumber);
    });
});

demo.Source1GameEvents.RoundEnd += e =>
{
    OnRoundEnd(demo, (CSRoundEndReason) e.Reason, (CSTeamNumber) e.Winner);
};
in0finite commented 9 months ago

That's too complicated only to detect round end. In my opinion, the library should provide an interface for this, so that end-users don't have to fallback to several different solutions based on demo version.

Look at Golang library, they provide many high-level APIs that wrap inner demo functions.

saul commented 9 months ago

I agree - I've just opened https://github.com/saul/demofile-net/pull/42 to address this.

I'm not too sure on the API though. I'm tempted to synthesise the old events on the same Source1GameEvents class.

in0finite commented 9 months ago

Yeah, to me, synthesizing old events seems good enough.

saul commented 9 months ago

This will be available shortly as v0.10.1