package csgo import ( "compress/bzip2" "context" "csgowtfd/ent" "csgowtfd/ent/match" "csgowtfd/ent/player" "csgowtfd/ent/stats" "fmt" "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs" "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common" "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/events" log "github.com/sirupsen/logrus" "io" "net/http" "sync" "time" ) type Demo struct { ShareCode string MatchId uint64 Url string } type DemoParser struct { demoQueue chan *Demo tempDir string db *ent.Client lock *sync.RWMutex } type DemoNotFoundError struct { error } func (p *DemoParser) Setup(db *ent.Client, lock *sync.RWMutex, worker int) error { p.demoQueue = make(chan *Demo, 1000) p.db = db p.lock = lock for i := 0; i < worker; i++ { go p.parseWorker() } return nil } func (p *DemoParser) ParseDemo(demo *Demo) error { select { case p.demoQueue <- demo: return nil default: return fmt.Errorf("queue full") } } func (p *DemoParser) downloadReplay(demo *Demo) (io.Reader, error) { log.Debugf("[DP] Downloading replay for %d", demo.MatchId) r, err := http.Get(demo.Url) if err != nil { return nil, err } if r.StatusCode != http.StatusOK { return nil, DemoNotFoundError{fmt.Errorf("demo not found")} } return bzip2.NewReader(r.Body), nil } func (p *DemoParser) getDBPlayer(demo *Demo, demoPlayer *common.Player) (*ent.Stats, error) { p.lock.RLock() tMatchPlayer, err := p.db.Stats.Query().WithMatches(func(q *ent.MatchQuery) { q.Where(match.ID(demo.MatchId)) }).WithPlayers(func(q *ent.PlayerQuery) { q.Where(player.ID(demoPlayer.SteamID64)) }).Only(context.Background()) p.lock.RUnlock() if err != nil { return nil, err } return tMatchPlayer, nil } func (p *DemoParser) getMatchPlayerBySteamID(stats []*ent.Stats, steamId uint64) *ent.Stats { for _, tStats := range stats { tPLayer, err := tStats.Edges.PlayersOrErr() if err != nil { log.Errorf("Unbale to get Stats from statList: %v", err) return nil } if tPLayer.ID == steamId { return tStats } } return nil } func (p *DemoParser) parseWorker() { for { select { case demo := <-p.demoQueue: if demo.MatchId == 0 { log.Warningf("[DP] can't parse match %s: no matchid found", demo.ShareCode) continue } p.lock.RLock() tMatch, err := p.db.Match.Query().Where(match.ID(demo.MatchId)).Only(context.Background()) p.lock.RUnlock() if err != nil { log.Errorf("[DP] Unable to get match %d: %v", demo.MatchId, err) continue } if tMatch.DemoParsed { log.Infof("[DP] skipped already parsed %d", demo.MatchId) continue } if tMatch.Date.Before(time.Now().UTC().AddDate(0, 0, -30)) { log.Infof("[DP] skipped already exipred demo for match %d", tMatch.ID) continue } startTime := time.Now() fDemo, err := p.downloadReplay(demo) if err != nil { switch e := err.(type) { case DemoNotFoundError: log.Infof("[DP] Demo 404 not found for %d. Trying agian later.", demo.MatchId) continue default: log.Errorf("[DP] Unable to download demo for %d: %v", demo.MatchId, e) continue } } downloadTime := time.Now().Sub(startTime) p.lock.RLock() tStats, err := tMatch.QueryStats().WithPlayers().All(context.Background()) p.lock.RUnlock() if err != nil { log.Errorf("[DP] Failed to find players for match %d: %v", demo.MatchId, err) continue } killMap := make(map[uint64]int, 10) eqMap := make(map[uint64][]*struct { Eq int HitGroup int Dmg uint To uint64 }) ecoMap := make(map[uint64][]*struct { Round int EqV int Bank int Spent int }, 0) demoParser := demoinfocs.NewParser(fDemo) // onPlayerHurt demoParser.RegisterEventHandler(func(e events.PlayerHurt) { if e.Attacker == nil || e.Player == nil || e.Weapon == nil || !demoParser.GameState().IsMatchStarted() { return } tAttacker := p.getMatchPlayerBySteamID(tStats, e.Attacker.SteamID64) if e.Attacker.Team == e.Player.Team { tAttacker.DmgTeam += uint(e.HealthDamageTaken) } else { tAttacker.DmgEnemy += uint(e.HealthDamageTaken) } if _, ok := eqMap[e.Attacker.SteamID64]; !ok { eqMap[e.Attacker.SteamID64] = make([]*struct { Eq int HitGroup int Dmg uint To uint64 }, 0) } found := false for _, di := range eqMap[e.Attacker.SteamID64] { if di.Eq == int(e.Weapon.Type) && di.HitGroup == int(e.HitGroup) { di.Dmg += uint(e.HealthDamageTaken) found = true } } if !found { eqMap[e.Attacker.SteamID64] = append(eqMap[e.Attacker.SteamID64], &struct { Eq int HitGroup int Dmg uint To uint64 }{Eq: int(e.Weapon.Type), HitGroup: int(e.HitGroup), Dmg: uint(e.HealthDamageTaken), To: e.Player.SteamID64}) } }) // onFreezeTimeEnd demoParser.RegisterEventHandler(func(e events.RoundEnd) { gs := demoParser.GameState() if !gs.IsMatchStarted() { return } for _, p := range gs.Participants().Playing() { ecoMap[p.SteamID64] = append(ecoMap[p.SteamID64], &struct { Round int EqV int Bank int Spent int }{Round: gs.TotalRoundsPlayed(), EqV: p.EquipmentValueCurrent(), Bank: p.Money(), Spent: p.MoneySpentThisRound()}) } }) // onRoundEnd demoParser.RegisterEventHandler(func(e events.RoundEnd) { if demoParser.GameState().IsMatchStarted() { for _, IGP := range demoParser.GameState().Participants().Playing() { if IGP != nil && IGP.SteamID64 != 0 { killDiff := IGP.Kills() - killMap[IGP.SteamID64] tPlayer := p.getMatchPlayerBySteamID(tStats, IGP.SteamID64) switch killDiff { case 2: tPlayer.Mk2++ case 3: tPlayer.Mk3++ case 4: tPlayer.Mk4++ case 5: tPlayer.Mk5++ } killMap[IGP.SteamID64] = IGP.Kills() } } } }) // onPlayerFlashed demoParser.RegisterEventHandler(func(e events.PlayerFlashed) { if e.Attacker == nil || e.Player == nil || !demoParser.GameState().IsMatchStarted() { return } tAttacker := p.getMatchPlayerBySteamID(tStats, e.Attacker.SteamID64) // team flash if e.Attacker.Team == e.Player.Team && e.Attacker.SteamID64 != e.Player.SteamID64 { tAttacker.FlashTotalTeam++ tAttacker.FlashDurationTeam += e.Player.FlashDuration } // own flash if e.Attacker.SteamID64 == e.Player.SteamID64 { tAttacker.FlashTotalSelf++ tAttacker.FlashDurationSelf += e.Player.FlashDuration } // enemy flash if e.Attacker.Team != e.Player.Team { tAttacker.FlashTotalEnemy++ tAttacker.FlashDurationEnemy += e.Player.FlashDuration } }) // onMatchStart demoParser.RegisterEventHandler(func(e events.MatchStart) { gs := demoParser.GameState() for _, demoPlayer := range gs.Participants().Playing() { if demoPlayer != nil && demoPlayer.SteamID64 != 0 { tMatchPlayer := p.getMatchPlayerBySteamID(tStats, demoPlayer.SteamID64) tMatchPlayer.Crosshair = demoPlayer.CrosshairCode() switch demoPlayer.Color() { case common.Yellow: tMatchPlayer.Color = stats.ColorYellow break case common.Green: tMatchPlayer.Color = stats.ColorGreen break case common.Purple: tMatchPlayer.Color = stats.ColorPurple break case common.Blue: tMatchPlayer.Color = stats.ColorBlue break case common.Orange: tMatchPlayer.Color = stats.ColorOrange break default: tMatchPlayer.Color = stats.ColorGrey } } } }) // onMatchEnd? demoParser.RegisterEventHandler(func(e events.AnnouncementWinPanelMatch) { }) // onRankUpdate demoParser.RegisterEventHandler(func(e events.RankUpdate) { if e.Player != nil && e.SteamID64() != 0 { tMatchPlayer := p.getMatchPlayerBySteamID(tStats, e.SteamID64()) tMatchPlayer.RankOld = e.RankOld tMatchPlayer.RankNew = e.RankNew } }) err = demoParser.ParseToEnd() if err != nil { log.Errorf("[DP] Error parsing replay: %v", err) continue } p.lock.Lock() err = tMatch.Update().SetMap(demoParser.Header().MapName).SetDemoParsed(true).Exec(context.Background()) p.lock.Unlock() if err != nil { log.Errorf("[DP] Unable to update match %d in database: %v", demo.MatchId, err) continue } for _, tMatchPlayer := range tStats { if tMatchPlayer.Color == "" { tMatchPlayer.Color = stats.ColorGrey } p.lock.Lock() nMatchPLayer, err := tMatchPlayer.Update(). SetDmgTeam(tMatchPlayer.DmgTeam). SetDmgEnemy(tMatchPlayer.DmgEnemy). SetUdHe(tMatchPlayer.UdHe). SetUdFlash(tMatchPlayer.UdFlash). SetUdSmoke(tMatchPlayer.UdSmoke). SetUdFlames(tMatchPlayer.UdFlames). SetMk2(tMatchPlayer.Mk2). SetMk3(tMatchPlayer.Mk3). SetMk4(tMatchPlayer.Mk4). SetMk5(tMatchPlayer.Mk5). SetRankOld(tMatchPlayer.RankOld). SetRankNew(tMatchPlayer.RankNew). SetColor(tMatchPlayer.Color). SetCrosshair(tMatchPlayer.Crosshair). SetFlashDurationTeam(tMatchPlayer.FlashDurationTeam). SetFlashDurationSelf(tMatchPlayer.FlashDurationSelf). SetFlashDurationEnemy(tMatchPlayer.FlashDurationEnemy). SetFlashTotalEnemy(tMatchPlayer.FlashTotalEnemy). SetFlashTotalSelf(tMatchPlayer.FlashTotalSelf). SetFlashTotalTeam(tMatchPlayer.FlashTotalTeam). SetDmgTeam(tMatchPlayer.DmgTeam). SetDmgEnemy(tMatchPlayer.DmgEnemy). Save(context.Background()) p.lock.Unlock() if err != nil { log.Errorf("[DP] Unable to update player %d in database: %v", tMatchPlayer.Edges.Players.ID, err) continue } for _, eqDmg := range eqMap[tMatchPlayer.PlayerStats] { p.lock.Lock() err := p.db.WeaponStats.Create().SetStat(nMatchPLayer).SetDmg(eqDmg.Dmg).SetVictim(eqDmg.To).SetHitGroup(eqDmg.HitGroup).SetEqType(eqDmg.Eq).Exec(context.Background()) p.lock.Unlock() if err != nil { log.Errorf("[DP] Unable to create WeaponStat: %v", err) } } for _, eco := range ecoMap[tMatchPlayer.PlayerStats] { p.lock.Lock() err := p.db.RoundStats.Create().SetStat(nMatchPLayer).SetRound(uint(eco.Round)).SetBank(uint(eco.Bank)).SetEquipment(uint(eco.EqV)).SetSpent(uint(eco.Spent)).Exec(context.Background()) p.lock.Unlock() if err != nil { log.Errorf("[DP] Unable to create WeaponStat: %v", err) } } } log.Infof("[DP] Parsed %d (took %s/%s)", demo.MatchId, downloadTime, time.Now().Sub(startTime)) err = demoParser.Close() if err != nil { log.Errorf("[DP] Unable close demo file for match %d: %v", demo.MatchId, err) } } } }