package csgo import ( "compress/bzip2" "context" "csgowtfd/ent" "csgowtfd/ent/match" "csgowtfd/ent/player" "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 Rank int Tickrate int File string } type DemoParser struct { demoQueue chan *Demo tempDir string } type DemoNotFoundError struct { error } func (p *DemoParser) Setup(db *ent.Client, lock *sync.RWMutex) error { p.demoQueue = make(chan *Demo, 1000) go p.parseWorker(db, lock) 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(db *ent.Client, lock *sync.RWMutex, demo *Demo, demoPlayer *common.Player) (*ent.Stats, error) { lock.RLock() tMatchPlayer, err := db.Stats.Query().WithMatches(func(q *ent.MatchQuery) { q.Where(match.MatchID(demo.MatchId)) }).WithPlayers(func(q *ent.PlayerQuery) { q.Where(player.Steamid(demoPlayer.SteamID64)) }).Only(context.Background()) 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.Steamid == steamId { return tStats } } return nil } func (p *DemoParser) parseWorker(db *ent.Client, lock *sync.RWMutex) { for { select { case demo := <-p.demoQueue: if demo.MatchId == 0 { log.Warningf("[DP] can't parse match %s: no matchid found", demo.ShareCode) continue } lock.RLock() tMatch, err := db.Match.Query().Where(match.MatchID(demo.MatchId)).Only(context.Background()) 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 } startTime := time.Now() fDemo, err := p.downloadReplay(demo) if err != nil { switch e := err.(type) { case DemoNotFoundError: err := tMatch.Update().SetDemoExpired(true).Exec(context.Background()) if err != nil { log.Errorf("[DP] Unable to set demo expire for match %d: %v", demo.MatchId, e) continue } log.Warningf("[DP] Demo already expired for %d", demo.MatchId) continue default: log.Warningf("[DP] Unable to download demo for %d: %v", demo.MatchId, e) continue } } downloadTime := time.Now().Sub(startTime) lock.RLock() tStats, err := tMatch.QueryStats().WithPlayers().All(context.Background()) 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) gameStarted := false demoParser := demoinfocs.NewParser(fDemo) // onPlayerHurt demoParser.RegisterEventHandler(func(e events.PlayerHurt) { if e.Attacker == nil || e.Player == nil || !gameStarted { return } tAttacker := p.getMatchPlayerBySteamID(tStats, e.Attacker.SteamID64) if e.Attacker.Team == e.Player.Team { tAttacker.Extended.Dmg.Team += e.HealthDamageTaken return } else { tAttacker.Extended.Dmg.Enemy += e.HealthDamageTaken } switch e.Weapon.Type { case common.EqDecoy: tAttacker.Extended.Dmg.UD.Decoy += e.HealthDamageTaken case common.EqSmoke: tAttacker.Extended.Dmg.UD.Smoke += e.HealthDamageTaken case common.EqHE: tAttacker.Extended.Dmg.UD.HE += e.HealthDamageTaken case common.EqMolotov, common.EqIncendiary: tAttacker.Extended.Dmg.UD.Flames += e.HealthDamageTaken case common.EqFlash: tAttacker.Extended.Dmg.UD.Flash += e.HealthDamageTaken } switch e.HitGroup { case events.HitGroupHead: tAttacker.Extended.Dmg.HitGroup.Head += e.HealthDamageTaken case events.HitGroupChest: tAttacker.Extended.Dmg.HitGroup.Chest += e.HealthDamageTaken case events.HitGroupStomach: tAttacker.Extended.Dmg.HitGroup.Stomach += e.HealthDamageTaken case events.HitGroupLeftArm: tAttacker.Extended.Dmg.HitGroup.LeftArm += e.HealthDamageTaken case events.HitGroupRightArm: tAttacker.Extended.Dmg.HitGroup.RightArm += e.HealthDamageTaken case events.HitGroupLeftLeg: tAttacker.Extended.Dmg.HitGroup.LeftLeg += e.HealthDamageTaken case events.HitGroupRightLeg: tAttacker.Extended.Dmg.HitGroup.RightLeg += e.HealthDamageTaken case events.HitGroupGear: tAttacker.Extended.Dmg.HitGroup.Gear += e.HealthDamageTaken } }) // onKill demoParser.RegisterEventHandler(func(e events.Kill) { }) // onFreezeTimeEnd demoParser.RegisterEventHandler(func(e events.RoundFreezetimeEnd) { }) // onRoundEnd demoParser.RegisterEventHandler(func(e events.RoundEnd) { if gameStarted { 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.Extended.MultiKills.Duo++ case 3: tPlayer.Extended.MultiKills.Triple++ case 4: tPlayer.Extended.MultiKills.Quad++ case 5: tPlayer.Extended.MultiKills.Pent++ } killMap[IGP.SteamID64] = IGP.Kills() } } } }) // onPlayerFlashed demoParser.RegisterEventHandler(func(e events.PlayerFlashed) { if e.Attacker == nil || e.Player == nil || !gameStarted { return } tAttacker := p.getMatchPlayerBySteamID(tStats, e.Attacker.SteamID64) // team flash if e.Attacker.Team == e.Player.Team && e.Attacker.SteamID64 != e.Player.SteamID64 { tAttacker.Extended.Flash.Total.Team++ tAttacker.Extended.Flash.Duration.Team += e.Player.FlashDuration } // own flash if e.Attacker.SteamID64 == e.Player.SteamID64 { tAttacker.Extended.Flash.Total.Self++ tAttacker.Extended.Flash.Duration.Self += e.Player.FlashDuration } // enemy flash if e.Attacker.Team != e.Player.Team { tAttacker.Extended.Flash.Total.Enemy++ tAttacker.Extended.Flash.Duration.Enemy += e.Player.FlashDuration } }) // onMatchStart demoParser.RegisterEventHandler(func(e events.MatchStart) { gs := demoParser.GameState() gameStarted = true for _, demoPlayer := range gs.Participants().Playing() { if demoPlayer != nil && demoPlayer.SteamID64 != 0 { tMatchPlayer := p.getMatchPlayerBySteamID(tStats, demoPlayer.SteamID64) tMatchPlayer.Extended.Crosshair = demoPlayer.CrosshairCode() tMatchPlayer.Extended.Color = int(demoPlayer.Color()) } } }) // 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.Extended.Rank.Old = e.RankOld tMatchPlayer.Extended.Rank.New = e.RankNew } }) err = demoParser.ParseToEnd() if err != nil { log.Errorf("[DP] Error parsing replay: %v", err) continue } lock.Lock() err = tMatch.Update().SetMap(demoParser.Header().MapName).SetDemoParsed(true).Exec(context.Background()) lock.Unlock() if err != nil { log.Errorf("[DP] Unable to update match %d in database: %v", demo.MatchId, err) continue } for _, tMatchPlayer := range tStats { lock.Lock() err := tMatchPlayer.Update().SetExtended(tMatchPlayer.Extended).Exec(context.Background()) lock.Unlock() if err != nil { log.Errorf("[DP] Unable to update player %d in database: %v", tMatchPlayer.Edges.Players.Steamid, err) continue } } 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) } } } }