341 lines
8.6 KiB
Go
341 lines
8.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 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 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"`
|
|
}
|
|
|
|
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 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)
|
|
playerInitialTeams := make(map[uint64]string)
|
|
|
|
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: "", // später gesetzt
|
|
}
|
|
playerStats[sid] = stat
|
|
}
|
|
return stat
|
|
}
|
|
|
|
p.RegisterEventHandler(func(e events.MatchStart) {
|
|
for _, player := range p.GameState().Participants().Playing() {
|
|
switch player.Team {
|
|
case common.TeamTerrorists:
|
|
playerInitialTeams[player.SteamID64] = "T"
|
|
case common.TeamCounterTerrorists:
|
|
playerInitialTeams[player.SteamID64] = "CT"
|
|
}
|
|
}
|
|
})
|
|
|
|
// Eventhandler (Kill, Hurt, etc.)
|
|
p.RegisterEventHandler(func(e events.Kill) {
|
|
if e.Killer != nil && e.Killer.SteamID64 != e.Victim.SteamID64 {
|
|
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)
|
|
stat.TotalDamage += e.HealthDamage
|
|
if e.Weapon != nil {
|
|
switch e.Weapon.Type {
|
|
case common.EqHE, common.EqIncendiary, common.EqMolotov:
|
|
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)
|
|
}
|
|
|
|
teamCT := p.GameState().TeamCounterTerrorists().ClanName()
|
|
if teamCT == "" {
|
|
teamCT = "CT"
|
|
}
|
|
teamT := p.GameState().TeamTerrorists().ClanName()
|
|
if teamT == "" {
|
|
teamT = "T"
|
|
}
|
|
|
|
winnerTeam := "Draw"
|
|
if scoreCT > scoreT {
|
|
winnerTeam = teamCT
|
|
} else if scoreT > scoreCT {
|
|
winnerTeam = teamT
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
// Final: Teamzuweisung korrekt, keine Überschreibung
|
|
for sid, stat := range playerStats {
|
|
if stat.Team == "" || stat.Team == "Unknown" {
|
|
if team, ok := playerInitialTeams[sid]; ok {
|
|
stat.Team = team
|
|
} else {
|
|
stat.Team = "Unknown"
|
|
}
|
|
}
|
|
result.Players = append(result.Players, *stat)
|
|
}
|
|
|
|
jsonData, err := json.Marshal(result)
|
|
if err != nil {
|
|
fmt.Printf("❌ Fehler beim JSON-Export: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println(string(jsonData))
|
|
}
|