package main import ( "encoding/json" "fmt" "math" "os" "time" 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 PlayerStats struct { Name string `json:"name"` SteamID string `json:"steamId"` Kills int `json:"kills"` Deaths int `json:"deaths"` Assists int `json:"assists"` FlashAssists int `json:"flashAssists"` 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"` } type RoundResult struct { Round int `json:"round"` Winner string `json:"winner"` WinReason string `json:"winReason"` } type DemoMeta struct { MatchID uint64 `json:"matchId"` Map string `json:"map"` Players []PlayerStats `json:"players"` Duration float64 `json:"duration"` TickRate float64 `json:"tickRate"` ScoreCT int `json:"scoreCT"` ScoreT int `json:"scoreT"` TeamCT string `json:"teamCT"` TeamT string `json:"teamT"` WinnerTeam string `json:"winnerTeam"` RoundCount int `json:"roundCount"` RoundHistory []RoundResult `json:"roundHistory"` DemoDate string `json:"demoDate"` } func sanitizeFloat(value float64) float64 { if math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return value } func getDemoTimestamp(path string) string { info, err := os.Stat(path) if err != nil { return "" } return info.ModTime().UTC().Format("2006-01-02T15:04:05Z") } 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 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 if len(os.Args) >= 3 { inputId := os.Args[2] _, err := fmt.Sscanf(inputId, "%d", &matchId) if err != nil { fmt.Printf("⚠️ Ungültige matchId-Argument: %v\n", err) matchId = 0 } } p.RegisterNetMessageHandler(func(msg *msg.CSVCMsg_ServerInfo) { if msg != nil && msg.GetMapName() != "" { mapName = msg.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(sid uint64, name string) *PlayerStats { stat := playerStats[sid] if stat == nil { stat = &PlayerStats{ Name: name, SteamID: fmt.Sprintf("%d", sid), } playerStats[sid] = stat } return stat } p.RegisterEventHandler(func(e events.Kill) { if e.Killer != nil && e.Killer.SteamID64 != e.Victim.SteamID64 { stat := getOrCreate(e.Killer.SteamID64, e.Killer.Name) stat.Kills++ 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 { stat := getOrCreate(e.Victim.SteamID64, e.Victim.Name) stat.Deaths++ } if e.Assister != nil { stat := getOrCreate(e.Assister.SteamID64, e.Assister.Name) stat.Assists++ } }) p.RegisterEventHandler(func(e events.PlayerFlashed) { if e.Attacker != nil && e.Attacker.SteamID64 != e.Player.SteamID64 { stat := getOrCreate(e.Attacker.SteamID64, e.Attacker.Name) stat.FlashAssists++ } }) p.RegisterEventHandler(func(e events.RoundMVPAnnouncement) { if e.Player != nil { stat := getOrCreate(e.Player.SteamID64, e.Player.Name) 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) { 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), }) }) p.RegisterEventHandler(func(e events.RankUpdate) { if e.Player != nil { stat := getOrCreate(e.Player.SteamID64, e.Player.Name) 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) } teamCT := p.GameState().TeamCounterTerrorists().ClanName() teamT := p.GameState().TeamTerrorists().ClanName() scoreCT = p.GameState().TeamCounterTerrorists().Score() scoreT = p.GameState().TeamTerrorists().Score() winnerTeam := "Draw" if scoreCT > scoreT { winnerTeam = teamCT } else if scoreT > scoreCT { winnerTeam = teamT } demoDate := getDemoTimestamp(filePath) if demoDate == "" { demoDate = time.Now().UTC().Format("2006-01-02T15:04:05Z") } 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, ScoreCT: scoreCT, ScoreT: scoreT, TeamCT: teamCT, TeamT: teamT, WinnerTeam: winnerTeam, RoundCount: roundCount, RoundHistory: roundHistory, DemoDate: demoDate, } for _, stat := range playerStats { result.Players = append(result.Players, *stat) } jsonData, err := json.Marshal(result) if err != nil { fmt.Printf("❌ Fehler beim JSON-Export: %v\n", err) fmt.Printf("⛔ Dump vor Fehler: %+v\n", result) os.Exit(1) } fmt.Println(string(jsonData)) }