2025-06-11 06:15:57 +02:00

315 lines
8.0 KiB
Go

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))
}