package main import ( "encoding/json" "fmt" "math" "os" demoinfocs "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" msg "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2" ) type Team struct { Name string `json:"name"` Score int `json:"score"` Players []PlayerStats `json:"players"` } type PlayerStats struct { Name string `json:"name"` SteamID string `json:"steamId"` Team string `json:"team"` Kills int `json:"kills"` Deaths int `json:"deaths"` Assists int `json:"assists"` FlashAssists int `json:"flashAssists"` TotalDamage int `json:"totalDamage"` UtilityDamage int `json:"utilityDamage"` MVPs int `json:"mvps"` MVPReason1 int `json:"mvpEliminations"` MVPReason2 int `json:"mvpDefuse"` MVPReason3 int `json:"mvpPlant"` KnifeKills int `json:"knifeKills"` ZeusKills int `json:"zeusKills"` WallbangKills int `json:"wallbangKills"` SmokeKills int `json:"smokeKills"` Headshots int `json:"headshots"` NoScopes int `json:"noScopes"` BlindKills int `json:"blindKills"` RankOld int `json:"rankOld,omitempty"` RankNew int `json:"rankNew,omitempty"` RankChange int `json:"rankChange,omitempty"` WinCount int `json:"winCount,omitempty"` OneK int `json:"oneK"` TwoK int `json:"twoK"` ThreeK int `json:"threeK"` FourK int `json:"fourK"` FiveK int `json:"fiveK"` ShotsFired int `json:"shotsFired"` ShotsHit int `json:"shotsHit"` Aim float64 `json:"aim"` // Prozent } type RoundResult struct { Round int `json:"round"` Winner string `json:"winner"` WinReason string `json:"winReason"` } type PlayerTeamHistory map[int]map[uint64]string type DemoMeta struct { MatchID uint64 `json:"matchId"` Map string `json:"map"` Duration float64 `json:"duration"` TickRate float64 `json:"tickRate"` WinnerTeam string `json:"winnerTeam"` RoundCount int `json:"roundCount"` RoundHistory []RoundResult `json:"roundHistory"` TeamA Team `json:"teamA"` TeamB Team `json:"teamB"` } func sanitizeFloat(value float64) float64 { if math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return value } func reasonToString(reason events.RoundEndReason) string { switch reason { case events.RoundEndReasonTargetBombed: return "Bomb" case events.RoundEndReasonTerroristsStopped: return "T eliminated" case events.RoundEndReasonCTStoppedEscape: return "CT stopped escape" case events.RoundEndReasonTerroristsEscaped: return "T escaped" case events.RoundEndReasonHostagesRescued: return "Hostages rescued" case events.RoundEndReasonCTWin: return "CT win" case events.RoundEndReasonTerroristsWin: return "T win" case events.RoundEndReasonDraw: return "Draw" case events.RoundEndReasonHostagesNotRescued: return "Hostages not rescued" case events.RoundEndReasonTerroristsNotEscaped: return "T not escaped" case events.RoundEndReasonBombDefused: return "Bomb defused" default: return "Unknown" } } func parseSteamID(id string) (uint64, error) { var sid uint64 _, err := fmt.Sscanf(id, "%d", &sid) return sid, err } // Bullet-Waffen (Schusswaffen) erkennen – keine Nades/Knife/Zeus/Equipment func isBulletWeapon(eq *common.Equipment) bool { if eq == nil { return false } switch eq.Type { case common.EqKnife, common.EqZeus, common.EqHE, common.EqMolotov, common.EqIncendiary, common.EqFlash, common.EqSmoke, common.EqDecoy: return false } cls := eq.Class() if cls == common.EqClassGrenade || cls == common.EqClassEquipment { return false } return true // Pistols/SMG/Rifles/Heavy/Sniper } func main() { if len(os.Args) < 2 { fmt.Println("❌ Demo-Datei fehlt") os.Exit(1) } filePath := os.Args[1] f, err := os.Open(filePath) if err != nil { fmt.Printf("❌ Datei konnte nicht geöffnet werden: %v\n", err) os.Exit(1) } defer f.Close() p := demoinfocs.NewParser(f) var mapName string var matchId uint64 var scoreCT, scoreT, roundCount int var roundHistory []RoundResult var teamHistory = make(PlayerTeamHistory) // Per-Runde Kills, die bis zum *nächsten* RoundStart gehalten werden var roundKills = make(map[uint64]int) // SteamID64 -> Kills in der aktuellen (zuletzt gestarteten) Runde var haveOpenRound bool // true ab erstem RoundStart nach MatchStart var lastNonSpecTeam = make(map[uint64]string) // "CT" oder "T" if len(os.Args) >= 3 { inputId := os.Args[2] _, err := fmt.Sscanf(inputId, "%d", &matchId) if err != nil { fmt.Printf("⚠️ Ungültiges matchId-Argument: %v\n", err) matchId = 0 } } p.RegisterNetMessageHandler(func(m *msg.CSVCMsg_ServerInfo) { if m != nil && m.GetMapName() != "" { mapName = m.GetMapName() } }) header, err := p.ParseHeader() if err != nil { fmt.Printf("❌ Header konnte nicht gelesen werden: %v\n", err) os.Exit(1) } playerStats := make(map[uint64]*PlayerStats) getOrCreate := func(player common.Player) *PlayerStats { sid := player.SteamID64 stat := playerStats[sid] if stat == nil { stat = &PlayerStats{ Name: player.Name, SteamID: fmt.Sprintf("%d", sid), Team: "", } playerStats[sid] = stat } return stat } // Hilfsfunktion: roundKills -> 1k/2k/3k/4k/5k übertragen flushRoundKills := func() { for sid, k := range roundKills { stat := playerStats[sid] if stat == nil { continue } switch { case k == 1: stat.OneK++ case k == 2: stat.TwoK++ case k == 3: stat.ThreeK++ case k == 4: stat.FourK++ case k >= 5: stat.FiveK++ } } roundKills = make(map[uint64]int) } p.RegisterEventHandler(func(e events.RoundStart) { if !p.GameState().IsMatchStarted() { return } if haveOpenRound { flushRoundKills() } haveOpenRound = true round := roundCount + 1 teamHistory[round] = map[uint64]string{} for _, pl := range p.GameState().Participants().Playing() { switch pl.Team { case common.TeamCounterTerrorists: teamHistory[round][pl.SteamID64] = "CT" lastNonSpecTeam[pl.SteamID64] = "CT" case common.TeamTerrorists: teamHistory[round][pl.SteamID64] = "T" lastNonSpecTeam[pl.SteamID64] = "T" } } }) p.RegisterEventHandler(func(e events.PlayerTeamChange) { if e.Player == nil { return } sid := e.Player.SteamID64 // Nur Nicht-Spectator übernehmen; Spectator/Unassigned löscht NICHT den letzten Stand switch e.NewTeam { case common.TeamCounterTerrorists: lastNonSpecTeam[sid] = "CT" case common.TeamTerrorists: lastNonSpecTeam[sid] = "T" } }) // Schüsse zählen (nur Schusswaffen) p.RegisterEventHandler(func(e events.WeaponFire) { if !haveOpenRound { return // Warmup ignorieren } if e.Shooter == nil || e.Weapon == nil { return } if !isBulletWeapon(e.Weapon) { return } getOrCreate(*e.Shooter).ShotsFired++ }) p.RegisterEventHandler(func(e events.Kill) { if e.Killer != nil && e.Victim != nil && e.Killer.SteamID64 != e.Victim.SteamID64 { killerTeam := e.Killer.Team victimTeam := e.Victim.Team if killerTeam != victimTeam && killerTeam != common.TeamSpectators { stat := getOrCreate(*e.Killer) stat.Kills++ // Nur zählen, wenn wir uns in/zwischen echten Runden befinden if haveOpenRound { roundKills[e.Killer.SteamID64]++ } if e.IsHeadshot { stat.Headshots++ } if e.NoScope { stat.NoScopes++ } if e.AttackerBlind { stat.BlindKills++ } if e.ThroughSmoke { stat.SmokeKills++ } if e.IsWallBang() { stat.WallbangKills++ } if e.Weapon != nil { switch e.Weapon.Type { case common.EqKnife: stat.KnifeKills++ case common.EqZeus: stat.ZeusKills++ } } } } if e.Victim != nil { getOrCreate(*e.Victim).Deaths++ } if e.Assister != nil { getOrCreate(*e.Assister).Assists++ } }) p.RegisterEventHandler(func(e events.PlayerFlashed) { if e.Attacker != nil && e.Attacker.SteamID64 != e.Player.SteamID64 { getOrCreate(*e.Attacker).FlashAssists++ } }) p.RegisterEventHandler(func(e events.PlayerHurt) { if e.Attacker != nil && e.Attacker != e.Player { stat := getOrCreate(*e.Attacker) // Utility-Damage separat zählen if e.Weapon != nil { switch e.Weapon.Type { case common.EqHE, common.EqMolotov, common.EqIncendiary: stat.UtilityDamage += e.HealthDamage } } // Treffer (nur Schusswaffen, nur Gegner, nur echte Runden) if haveOpenRound && e.HealthDamage > 0 && e.Weapon != nil && isBulletWeapon(e.Weapon) { attTeam := e.Attacker.Team vicTeam := e.Player.Team if attTeam != vicTeam && attTeam != common.TeamSpectators { stat.ShotsHit++ } } } }) p.RegisterEventHandler(func(e events.RoundMVPAnnouncement) { if e.Player != nil { stat := getOrCreate(*e.Player) stat.MVPs++ switch e.Reason { case events.MVPReasonMostEliminations: stat.MVPReason1++ case events.MVPReasonBombDefused: stat.MVPReason2++ case events.MVPReasonBombPlanted: stat.MVPReason3++ } } }) p.RegisterEventHandler(func(e events.RoundEnd) { // Scores & Round-History wie gehabt scoreCT = p.GameState().TeamCounterTerrorists().Score() scoreT = p.GameState().TeamTerrorists().Score() roundCount++ var winner string switch e.Winner { case common.TeamTerrorists: winner = "T" case common.TeamCounterTerrorists: winner = "CT" default: winner = "Unknown" } roundHistory = append(roundHistory, RoundResult{ Round: roundCount, Winner: winner, WinReason: reasonToString(e.Reason), }) // KEIN flush hier! Nach-Runden-Kills sollen noch in diese Runde fallen. }) p.RegisterEventHandler(func(e events.RankUpdate) { if e.Player != nil { stat := getOrCreate(*e.Player) stat.RankOld = e.RankOld stat.RankNew = e.RankNew stat.RankChange = int(e.RankChange) stat.WinCount = e.WinCount } }) err = p.ParseToEnd() if err != nil { fmt.Printf("❌ Fehler beim Parsen: %v\n", err) os.Exit(1) } // Letztes Flush falls Match endet ohne weiteren RoundStart if haveOpenRound && len(roundKills) > 0 { flushRoundKills() } teamAName := p.GameState().TeamCounterTerrorists().ClanName() if teamAName == "" { teamAName = "CT" } teamBName := p.GameState().TeamTerrorists().ClanName() if teamBName == "" { teamBName = "T" } for _, stat := range playerStats { sid, _ := parseSteamID(stat.SteamID) if t, ok := lastNonSpecTeam[sid]; ok && (t == "CT" || t == "T") { stat.Team = t continue } // Fallback: letzter Runden-Eintrag aus teamHistory lastTeam := "" lastRound := 0 for roundNum, roundTeams := range teamHistory { if team, ok := roundTeams[sid]; ok && roundNum > lastRound { lastTeam = team lastRound = roundNum } } stat.Team = lastTeam } for _, pl := range p.GameState().Participants().All() { sid := pl.SteamID64 stat, ok := playerStats[sid] if !ok || pl.Entity == nil { continue } if val, ok := pl.Entity.PropertyValue("m_pActionTrackingServices.m_iDamage"); ok { stat.TotalDamage = val.Int() } } // Aim-Wert berechnen for _, stat := range playerStats { if stat.ShotsFired > 0 { stat.Aim = sanitizeFloat(float64(stat.ShotsHit) * 100.0 / float64(stat.ShotsFired)) } else { stat.Aim = 0 } } var ctPlayers, tPlayers []PlayerStats for _, stat := range playerStats { switch stat.Team { case "CT": ctPlayers = append(ctPlayers, *stat) case "T": tPlayers = append(tPlayers, *stat) } } winnerTeam := "Draw" if scoreCT > scoreT { winnerTeam = teamAName } else if scoreT > scoreCT { winnerTeam = teamBName } duration := sanitizeFloat(header.PlaybackTime.Seconds()) tickRate := 0.0 if duration > 0 { tickRate = sanitizeFloat(float64(header.PlaybackTicks) / duration) } if mapName == "" { mapName = header.MapName } result := DemoMeta{ MatchID: matchId, Map: mapName, Duration: duration, TickRate: tickRate, WinnerTeam: winnerTeam, RoundCount: roundCount, RoundHistory: roundHistory, TeamA: Team{ Name: teamAName, Score: scoreCT, Players: ctPlayers, }, TeamB: Team{ Name: teamBName, Score: scoreT, Players: tPlayers, }, } jsonData, err := json.Marshal(result) if err != nil { fmt.Printf("❌ Fehler beim JSON-Export: %v\n", err) os.Exit(1) } fmt.Println(string(jsonData)) }