2025-07-31 17:42:19 +02:00

391 lines
9.6 KiB
Go

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"`
}
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
}
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)
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(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
}
p.RegisterEventHandler(func(e events.RoundStart) {
round := roundCount + 1 // nächste Runde
teamHistory[round] = map[uint64]string{}
for _, p := range p.GameState().Participants().Playing() {
if p.Team == common.TeamCounterTerrorists {
teamHistory[round][p.SteamID64] = "CT"
} else if p.Team == common.TeamTerrorists {
teamHistory[round][p.SteamID64] = "T"
}
}
})
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++
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)
stat.Deaths++
}
if e.Assister != nil {
stat := getOrCreate(*e.Assister)
stat.Assists++
}
})
p.RegisterEventHandler(func(e events.PlayerFlashed) {
if e.Attacker != nil && e.Attacker.SteamID64 != e.Player.SteamID64 {
stat := getOrCreate(*e.Attacker)
stat.FlashAssists++
}
})
p.RegisterEventHandler(func(e events.PlayerHurt) {
if e.Attacker != nil && e.Attacker != e.Player {
stat := getOrCreate(*e.Attacker)
if e.Weapon != nil {
switch e.Weapon.Type {
case common.EqHE, common.EqMolotov, common.EqIncendiary:
stat.UtilityDamage += e.HealthDamage
}
}
}
})
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) {
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)
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)
}
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)
lastTeam := ""
lastRound := 0
for roundNum, roundTeams := range teamHistory {
if team, ok := roundTeams[sid]; ok && roundNum > lastRound {
lastTeam = team
lastRound = roundNum
}
}
stat.Team = lastTeam
}
for _, player := range p.GameState().Participants().All() {
sid := player.SteamID64
stat, ok := playerStats[sid]
if !ok || player.Entity == nil {
continue
}
val, ok := player.Entity.PropertyValue("m_pActionTrackingServices.m_iDamage")
if ok {
stat.TotalDamage = val.Int()
}
}
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))
}