demoinfocs-golang icon indicating copy to clipboard operation
demoinfocs-golang copied to clipboard

Players are assigned to the wrong teams

Open JuhaKiili opened this issue 1 year ago • 4 comments
trafficstars

Describe the bug Players are assigned to wrong teams (T vs. CT). Sometimes there can be 9 players on CT and just one on T for example.

To Reproduce players-in-wrong-team-rmr-1.dem players-in-wrong-team-rmr-2.dem

Code used for printing out player state: go run test.go -demo /path/to/demo.dem

package main

import (
	"fmt"
	"os"

	ex "github.com/markus-wa/demoinfocs-golang/v4/examples"
	demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs"
	common "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common"
	events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events"
)

// Run like this: go run test.go -demo /path/to/demo.dem
func main() {
	f, err := os.Open(ex.DemoPathFromArgs())
	checkError(err)

	defer f.Close()

	p := demoinfocs.NewParser(f)
	defer p.Close()

	// Parse header
	header, err := p.ParseHeader()
	checkError(err)
	fmt.Println("Map:", header.MapName)

	p.RegisterEventHandler(func(e events.RoundStart) {
		gs := p.GameState()
		fmt.Printf("\nRound %d\n", gs.TotalRoundsPlayed()+1)

		fmt.Println("PARTICIPANTS:")
		for _, player := range gs.Participants().All() {
			fmt.Printf("PLAYER %s (%d) Team: %d Connected: %t\n", formatPlayer(player), player.SteamID64, player.Team, player.IsConnected)
		}
	})

	err = p.ParseToEnd()
	checkError(err)
}

func formatPlayer(p *common.Player) string {
	if p == nil {
		return "?"
	}

	switch p.Team {
	case common.TeamTerrorists:
		return "[T]" + p.Name
	case common.TeamCounterTerrorists:
		return "[CT]" + p.Name
	}

	return p.Name
}

func checkError(err error) {
	if err != nil {
		panic(err)
	}
}

Expected behavior

$ go run test.go -demo /path/to/players-in-wrong-team-rmr-1.dem

Round 1
PARTICIPANTS:
PLAYER [CT]koh415 (76561198159074559) Team: 3 Connected: true
PLAYER [T]Klarje (76561198097422106) Team: 2 Connected: true
PLAYER [CT]astonishred (76561198112681840) Team: 3 Connected: true
PLAYER [CT]ryncs (76561198328604687) Team: 3 Connected: true
PLAYER [CT]FanaticDuck (76561198302499325) Team: 3 Connected: true
PLAYER [CT]clutchllama (76561198094509053) Team: 3 Connected: true
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [CT]kolsy (76561198212592484) Team: 3 Connected: true
PLAYER [CT]milo_916 (76561198172505994) Team: 3 Connected: true
PLAYER [CT]ghost_solder (76561198029761801) Team: 3 Connected: true
PLAYER [CT]signal1 (76561198054121335) Team: 3 Connected: true
PLAYER GOTV (0) Team: 0 Connected: false
...

CURRENT: 9 players on CT, only 1 on T side EXPECTED: Normal 5 vs. 5

$ go run test.go -demo /path/to/players-in-wrong-team-rmr-2.dem

Round 1
PARTICIPANTS:
PLAYER [T]Kollo (76561198074134659) Team: 2 Connected: true
PLAYER [T]AwaykeN (76561198440676599) Team: 2 Connected: true
PLAYER [T]Duplicate (76561198152075689) Team: 2 Connected: true
PLAYER [CT]LYNXi (76561198083574557) Team: 3 Connected: true
PLAYER [CT]welhoboy87 (76561198181320650) Team: 3 Connected: true
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [CT]monoxx (76561198149834476) Team: 3 Connected: true
PLAYER [CT]bevvexyz (76561199103903099) Team: 3 Connected: true
PLAYER [CT]adam9130 (76561198040547485) Team: 3 Connected: true
PLAYER [CT]p12 (76561197966038702) Team: 3 Connected: true
PLAYER [T]doblean (76561198249125106) Team: 2 Connected: true
PLAYER GOTV (0) Team: 0 Connected: false
...

CURRENT: 6 players on CT, only 4 on T side EXPECTED: Normal 5 vs. 5

Library version latest in master 935fd38f2da60d196f4cbd3a107b181caa69e08c

Additional context

  • Both demos are from latest RMR qualifiers where there were some problems with the servers
  • For players-in-wrong-team-rmr-1.dem user says the demo works perfectly on Skybox Edge (can't confirm myself)

JuhaKiili avatar Jan 09 '24 06:01 JuhaKiili

This problem occurs for most demos from the RMR qualifiers.

Two randomly chosen examples from hltv:

Demo: nixuh-vs-bravado

Round 1
PARTICIPANTS:
PLAYER [T]march_ (76561198051922966) Team: 2 Connected: false
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [CT]Fadey (76561198141759568) Team: 3 Connected: true
PLAYER [T]RustyYG (76561198248601350) Team: 2 Connected: true
PLAYER [T]trit9n (76561198096199303) Team: 2 Connected: true
PLAYER [T]kaniioh (76561197992995849) Team: 2 Connected: true
PLAYER [CT]FROZ3Ncs (76561198070468233) Team: 3 Connected: true
PLAYER [T]IDoruI (76561198096170937) Team: 2 Connected: true
PLAYER [T]bvdwilj (76561198143694974) Team: 2 Connected: true
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [T]_Spitfire_ (76561198951001059) Team: 2 Connected: true
PLAYER [CT]flexeeee (76561198138004745) Team: 3 Connected: true
...

Demo: fnatic-vs-havu

Round 1
PARTICIPANTS:
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [CT]kyuubii (76561198144926364) Team: 3 Connected: true
PLAYER [CT]Matys176 (76561199046478501) Team: 3 Connected: false
PLAYER GOTV (0) Team: 0 Connected: false
PLAYER [T]AIRAX666 (76561198010426775) Team: 2 Connected: true
PLAYER [T]Banjo7 (76561198276486363) Team: 2 Connected: true
PLAYER [CT]krimz1337 (76561198031651584) Team: 3 Connected: true
PLAYER [CT]ULI (76561198012528425) Team: 3 Connected: true
PLAYER [CT]ottond (76561198035334871) Team: 3 Connected: true
PLAYER [CT]bodyy (76561198013295375) Team: 3 Connected: true
PLAYER [CT]afrocs2 (76561198035581101) Team: 3 Connected: true
PLAYER [T]sLowiCSGO (76561197976197744) Team: 2 Connected: true
...

JuhaKiili avatar Jan 09 '24 19:01 JuhaKiili

Info from the tournament organizer:

  • They used some kind of mod to enforce teams, which may or may not be the culprit
  • These demos apparently work fine with https://github.com/akiver/cs-demo-analyzer

JuhaKiili avatar Jan 09 '24 21:01 JuhaKiili

I replayed the players-in-wrong-team-rmr-1.dem and I added a print:

		if pl.TeamState != nil {
			fmt.Printf("bindNewPlayerControllerS2 %s\tTeamState.Team %d Team %d\n", pl.Name, pl.TeamState.Team(), pl.Team)
		}

inside controllerEntity.Property("m_iTeamNum").OnUpdate(func(val st.PropertyValue) https://github.com/markus-wa/demoinfocs-golang/blob/935fd38f2da60d196f4cbd3a107b181caa69e08c/pkg/demoinfocs/datatables.go#L523C4-L523C4

This was the result of that print:

bindNewPlayerControllerS2 kolsy         TeamState.Team 2 Team 2
bindNewPlayerControllerS2 koh415        TeamState.Team 3 Team 3
bindNewPlayerControllerS2 milo_916      TeamState.Team 3 Team 3
bindNewPlayerControllerS2 Klarje        TeamState.Team 2 Team 2
bindNewPlayerControllerS2 astonishred   TeamState.Team 2 Team 2
bindNewPlayerControllerS2 ryncs         TeamState.Team 2 Team 2
bindNewPlayerControllerS2 ghost_solder  TeamState.Team 3 Team 3
bindNewPlayerControllerS2 FanaticDuck   TeamState.Team 3 Team 3
bindNewPlayerControllerS2 signal1       TeamState.Team 2 Team 2
bindNewPlayerControllerS2 clutchllama   TeamState.Team 3 Team 3

So I think the information is comming alright.

I also find out that player.Team != player.TeamState.Team(). With similar to your print:

	p.RegisterEventHandler(func(e events.RoundStart) {
		gs := p.GameState()
		fmt.Printf("\nRound %d\n", gs.TotalRoundsPlayed()+1)

		fmt.Println("PARTICIPANTS:")
		for _, player := range gs.Participants().All() {
			if !player.IsBot{
				fmt.Printf("PLAYER %s\t(%d)\tTeam: %d\tTeamState.Team: %d\n", formatPlayer(player), player.SteamID64, player.Team, player.TeamState.Team())
			}
		}
	}
Round 1
PARTICIPANTS:
PLAYER [CT] clutchllama (76561198094509053)     Team: 3 TeamState.Team: 3
PLAYER [CT] kolsy       (76561198212592484)     Team: 3 TeamState.Team: 2
PLAYER [CT] koh415      (76561198159074559)     Team: 3 TeamState.Team: 3
PLAYER [CT] milo_916    (76561198172505994)     Team: 3 TeamState.Team: 3
PLAYER [CT] ghost_solder        (76561198029761801)     Team: 3 TeamState.Team: 3
PLAYER [CT] FanaticDuck (76561198302499325)     Team: 3 TeamState.Team: 3
PLAYER [T]  Klarje      (76561198097422106)     Team: 2 TeamState.Team: 2
PLAYER [CT] astonishred (76561198112681840)     Team: 3 TeamState.Team: 2
PLAYER [CT] ryncs       (76561198328604687)     Team: 3 TeamState.Team: 2
PLAYER [CT] signal1     (76561198054121335)     Team: 3 TeamState.Team: 2

I don't know how to fix it. But I hope this might help.

CapekM avatar Jan 18 '24 08:01 CapekM

We're very much experiencing the same. Currently using Anubis from this match for debugging: https://www.hltv.org/matches/2368983/500-vs-ecstatic-pgl-cs2-major-copenhagen-2024-europe-rmr-open-qualifier-3

micvbang avatar Jan 22 '24 18:01 micvbang

Info from the tournament organizer: They used some kind of mod to enforce teams, which may or may not be the culprit These demos apparently work fine with https://github.com/akiver/cs-demo-analyzer

@akiver how do you read the teams there? maybe we can copy that approach :eyes:

markus-wa avatar Feb 02 '24 11:02 markus-wa

this PR should fix the problem https://github.com/markus-wa/demoinfocs-golang/pull/499, could you try on this branch please?

akiver avatar Feb 02 '24 23:02 akiver

this PR should fix the problem #499, could you try on this branch please?

This seems to fix it for all demos :+1:

JuhaKiili avatar Feb 05 '24 06:02 JuhaKiili

this PR should fix the problem #499, could you try on this branch please?

This seems to fix it for all demos 👍

I unfortunately disagree - I've got a few demos it doesn't work for. I'm not at the right PC at the moment, so I'll post an example or two tonight

micvbang avatar Feb 05 '24 06:02 micvbang

So, here are the examples:

In anubis of this match, the teams are incorrectly reported to only include 3 and 4 players respectively when fetching data from the game state. Here's an example:

package main

import (
	"fmt"
	"log"
	"os"
	"slices"

	dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs"
	"github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/common"
	"github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events"
)

func main() {
	testFile := "pgl-cs2-major-copenhagen-2024-europe-rmr-open-qualifier-3-500-vs-ecstatic-bo3-M9LZlEbSCkGLCNY1nbFxsI/500-ecstatic-m1-anubis.dem"

	f, err := os.Open(testFile)
	requireNoError(err)

	p := dem.NewParser(f)
	round := 0
	p.RegisterEventHandler(func(e events.RoundEndOfficial) {
		round += 1
		fmt.Printf("round: %d\n", round)
		for _, t := range []common.Team{common.TeamCounterTerrorists, common.TeamTerrorists} {
			players := make([]string, 0, 5)
			for _, p := range p.GameState().Participants().TeamMembers(t) {
				players = append(players, p.Name)
			}
			slices.Sort(players)
			fmt.Printf("\tteam: %v: %+v\n", t, players)
		}
	})

	err = p.ParseToEnd()
	requireNoError(err)
}

func requireNoError(err error) {
	if err != nil {
		log.Fatalf("error: %s", err)
	}
}

Here are examples of the output I'm getting when running it locally:

round: 5                                                             
        team: 3: [Kraghen Nodios Pattiaten Queenix salazar]
        team: 2: [Patrick Rainwaker dennyslaw]
round: 6                                                             
        team: 3: [Kraghen Nodios Queenix salazar]
        team: 2: [Patrick Rainwaker dennyslaw]

The problem seems to be that the data in the game state isn't being updated correctly. The fix we're currently relying on is to grab the data directly from CCSTeam.m_aPlayers in pretty much the same way that we're doing it for weapons. This obviously does not produce the expected PlayerTeamChange event, nor is it a nice solution in general. But that's our fix so far

p.stParser.ServerClasses().FindByName("CCSTeam").OnEntityCreated(func(entity st.Entity) {
	...

	if !p.isSource2() {
		return
	}

	// TODO: this is a major hack to track player teams without having
	// to refactor the whole handling of player team states.
	var teamSize uint64 = 16
	internalMembers := make([]*common.Player, 64)

	setTeamPlayers := func() {
		members := make([]*common.Player, 0, teamSize)
		for i := uint64(0); i < teamSize; i++ {
			player := internalMembers[i]
			if player == nil || player.IsUnknown {
				continue
			}

			members = append(members, player)
		}
		// TODO: members now holds the correct team members. We're currently shoving
		// this into `TeamState`, circumventing having to fiddle with the Participants callback.
	}

	entity.Property("m_aPlayers").OnUpdate(func(pv st.PropertyValue) {
		// NOTE: Sometimes pv.Any contains an array with bitmasked values
		// that can be converted to actual player ids with entityIDFromHandle([value], true).
		//
		// We've only experienced this the first time the value is updated.
		size, ok := pv.Any.(uint64)
		if !ok {
			return
		}

		teamSize = size
		setTeamPlayers()
	})

	for i := 0; i < 32; i++ {
		i := i
		slot := fmt.Sprintf("m_aPlayers.%04d", i)
		entity.Property(slot).OnUpdate(func(pv st.PropertyValue) {
			var player *common.Player

			if pv.Any != nil {
				playerEntityID := entityIDFromHandle(pv.S2UInt64(), true)
				player = p.gameState.playersByEntityID[playerEntityID]
				player.Team = s.Team()

				internalMembers[i] = player
			}

			setTeamPlayers()
		})
	}
}

micvbang avatar Feb 06 '24 18:02 micvbang

I have an additional example of a match with some an edge case that requires handling the team size correctly:

micvbang avatar Feb 06 '24 19:02 micvbang

@micvbang could you try with the last commit from https://github.com/markus-wa/demoinfocs-golang/pull/499?
Some players were seen as disconnected I think that's why they were missing.

akiver avatar Feb 07 '24 01:02 akiver

@micvbang could you try with the last commit from #499? Some players were seen as disconnected I think that's why they were missing.

Thank you! Yes, this looks promising! I haven't been able to spot any issues with it

micvbang avatar Feb 07 '24 19:02 micvbang

I will be closing this for now, feel free to reopen if the issue persists with v4.0.2

markus-wa avatar Feb 09 '24 10:02 markus-wa