[FEATURE] Detailed stats about player weapon usage and hitgroups (#1)

Reviewed-on: https://git.harting.dev/CSGOWTF/csgowtfd/pulls/1
Co-authored-by: Giovanni Harting <539@idlegandalf.com>
Co-committed-by: Giovanni Harting <539@idlegandalf.com>
This commit is contained in:
2021-10-16 01:49:52 +02:00
parent 3ff65bc5d7
commit 99ec0ad1bc
37 changed files with 15185 additions and 1080 deletions

View File

@@ -7,7 +7,6 @@ import (
"csgowtfd/ent/player"
"csgowtfd/ent/stats"
"encoding/json"
"encoding/xml"
"entgo.io/ent/dialect/sql"
"fmt"
"github.com/Philipp15b/go-steamapi"
@@ -16,6 +15,7 @@ import (
"io"
"io/ioutil"
"net/http"
"path"
"regexp"
"strconv"
"strings"
@@ -83,6 +83,110 @@ type MatchStats struct {
Loss int `json:"loss,omitempty"`
}
type MultiKills struct {
Duo uint `json:"duo,omitempty"`
Triple uint `json:"triple,omitempty"`
Quad uint `json:"quad,omitempty"`
Pent uint `json:"pent,omitempty"`
}
type Rank struct {
Old int `json:"old,omitempty"`
New int `json:"new,omitempty"`
}
type HitGroup struct {
Head uint `json:"head,omitempty"`
Chest uint `json:"chest,omitempty"`
Stomach uint `json:"stomach,omitempty"`
LeftArm uint `json:"left_arm,omitempty"`
RightArm uint `json:"right_arm,omitempty"`
LeftLeg uint `json:"left_leg,omitempty"`
RightLeg uint `json:"right_leg,omitempty"`
Gear uint `json:"gear,omitempty"`
}
type UD struct {
HE uint `json:"he,omitempty"`
Flames uint `json:"flames,omitempty"`
Flash uint `json:"flash,omitempty"`
Decoy uint `json:"decoy,omitempty"`
Smoke uint `json:"smoke,omitempty"`
}
type Damage struct {
Enemy uint `json:"enemy,omitempty"`
Team uint `json:"team,omitempty"`
UD *UD `json:"ud,omitempty"`
HitGroup *HitGroup `json:"hit_group,omitempty"`
}
type SelfTeamEnemy struct {
Self interface{} `json:"self,omitempty"`
Team interface{} `json:"team,omitempty"`
Enemy interface{} `json:"enemy,omitempty"`
}
type Flash struct {
Duration *SelfTeamEnemy `json:"duration,omitempty"`
Total *SelfTeamEnemy `json:"total,omitempty"`
}
type StatsResponse struct {
TeamID int `json:"team_id"`
Kills int `json:"kills"`
Deaths int `json:"deaths"`
Assists int `json:"assists"`
Headshot int `json:"headshot"`
MVP uint `json:"mvp"`
Score int `json:"score"`
Player interface{} `json:"player,omitempty"`
Rank *Rank `json:"rank,omitempty"`
MultiKills *MultiKills `json:"multi_kills,omitempty"`
Dmg *Damage `json:"dmg,omitempty"`
Flash *Flash `json:"flash,omitempty"`
Crosshair string `json:"crosshair,omitempty"`
Color string `json:"color,omitempty"`
KAST int `json:"kast,omitempty"`
}
type PlayerResponse struct {
SteamID64 uint64 `json:"steamid64,string"`
Name string `json:"name"`
Avatar string `json:"avatar"`
VAC bool `json:"vac"`
VACDate *time.Time `json:"vac_date,omitempty"`
Tracked bool `json:"tracked"`
VanityURL string `json:"vanity_url,omitempty"`
MatchStats MatchStats `json:"match_stats,omitempty"`
Matches []*MatchResponse `json:"matches,omitempty"`
}
type EqResponse struct {
Victim uint64 `json:"victim"`
Type int `json:"type"`
HitGroup int `json:"hit_group"`
Dmg uint `json:"dmg"`
}
type WeaponResponse struct {
Player *PlayerResponse `json:"player"`
Eq []*EqResponse `json:"eq,omitempty"`
}
type MatchResponse struct {
MatchId uint64 `json:"match_id,string"`
ShareCode string `json:"share_code,omitempty"`
Map string `json:"map"`
Date time.Time `json:"date"`
Score [2]int `json:"score"`
Duration int `json:"duration"`
MatchResult int `json:"match_result"`
MaxRounds int `json:"max_rounds,omitempty"`
Parsed bool `json:"parsed"`
Stats interface{} `json:"stats,omitempty"`
}
const (
steamID64Entry = "https://steamcommunity.com/profiles/%d?xml=1"
steamVanityURLEntry = "https://steamcommunity.com/id/%s?xml=1"
@@ -262,13 +366,13 @@ func GetPlayer(db *DBWithLock, id interface{}, apiKey string, rl ratelimit.Limit
return GetPlayerFromSteamID64(db, steamID64, apiKey, rl)
}
return GetPlayerFromVanityURL(db, e)
return GetPlayerFromVanityURL(db, e, apiKey, rl)
default:
return nil, fmt.Errorf("invalid arguments")
}
}
func GetPlayerFromVanityURL(db *DBWithLock, id string) (*ent.Player, error) {
func GetPlayerFromVanityURL(db *DBWithLock, id string, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) {
if id == "" {
return nil, fmt.Errorf("invalid arguments")
}
@@ -279,28 +383,28 @@ func GetPlayerFromVanityURL(db *DBWithLock, id string) (*ent.Player, error) {
if err == nil {
return tPlayer, nil
} else {
profile, err := SteamProfile2XML(id, 0)
rl.Take()
resp, err := steamapi.ResolveVanityURL(id, apiKey)
if err != nil {
return nil, err
}
if profile.Error != "" {
return nil, fmt.Errorf("profile not found")
if resp.Success != 1 {
return nil, fmt.Errorf("vanity url not found")
}
db.Lock.Lock()
nPlayer, err := db.Client.Player.Create().SetID(profile.SteamID64).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetName(profile.ProfileName).Save(context.Background())
db.Lock.Unlock()
nPlayer, err := GetPlayerFromSteamID64(db, resp.SteamID, apiKey, rl)
if err != nil {
return nil, err
}
return nPlayer, nil
}
}
func GetPlayerFromSteamID64(db *DBWithLock, steamID uint64, apiKey string, rl ratelimit.Limiter) (*ent.Player, error) {
db.Lock.RLock()
tPlayer, err := db.Client.Player.Query().Where(player.ID(steamID)).Only(context.Background())
tPlayer, err := db.Client.Player.Get(context.Background(), steamID)
db.Lock.RUnlock()
if err == nil {
return tPlayer, nil
@@ -312,75 +416,78 @@ func GetPlayerFromSteamID64(db *DBWithLock, steamID uint64, apiKey string, rl ra
return nil, err
}
nPlayer, err = UpdatePlayerFromSteam(nPlayer, apiKey, db.Lock, rl)
uPlayer, err := UpdatePlayerFromSteam([]*ent.Player{nPlayer}, db.Client, apiKey, db.Lock, rl)
if err != nil {
return nil, err
}
return nPlayer, nil
if len(uPlayer) > 0 {
return uPlayer[0], nil
} else {
return nil, nil
}
}
}
func SteamProfile2XML(id string, steamID64 uint64) (*CommunityXML, error) {
var r *http.Response
var err error
if steamID64 != 0 {
r, err = http.Get(fmt.Sprintf(steamID64Entry, steamID64))
} else {
r, err = http.Get(fmt.Sprintf(steamVanityURLEntry, id))
}
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(r.Body)
func UpdatePlayerFromSteam(players []*ent.Player, db *ent.Client, apiKey string, lock *sync.RWMutex, rl ratelimit.Limiter) ([]*ent.Player, error) {
var idsToUpdate []uint64
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
cXML := &CommunityXML{}
err = xml.Unmarshal(body, cXML)
if err != nil {
return nil, err
}
return cXML, nil
}
func UpdatePlayerFromSteam(player *ent.Player, apiKey string, lock *sync.RWMutex, rl ratelimit.Limiter) (*ent.Player, error) {
profile, err := SteamProfile2XML("", player.ID)
if err != nil {
return nil, err
}
if profile.Error != "" {
return nil, fmt.Errorf("profile not found")
}
lock.Lock()
tPlayer, err := player.Update().SetName(profile.ProfileName).SetVac(profile.VacBanned).SetAvatarURL(profile.AvatarURL).SetSteamUpdated(time.Now().UTC()).SetVanityURL(strings.ToLower(profile.VanityURL)).SetVanityURLReal(profile.VanityURL).SetSteamUpdated(time.Now().UTC()).Save(context.Background())
lock.Unlock()
if err != nil {
return nil, err
for _, updatePlayer := range players {
idsToUpdate = append(idsToUpdate, updatePlayer.ID)
}
rl.Take()
bans, err := steamapi.GetPlayerBans([]uint64{profile.SteamID64}, apiKey)
playerSum, err := steamapi.GetPlayerSummaries(idsToUpdate, apiKey)
if err != nil {
return nil, err
}
if len(bans) > 0 && bans[0].NumberOfVACBans > 0 {
banDate := time.Now().UTC().AddDate(0, 0, -1*int(bans[0].DaysSinceLastBan))
var nPlayers []*ent.Player
for _, pS := range playerSum {
// TODO: what happens if a player deleted their profile?
// check for vanityURL
if SteamId64RegEx.MatchString(path.Base(pS.ProfileURL)) {
pS.ProfileURL = ""
} else {
pS.ProfileURL = path.Base(pS.ProfileURL)
}
lock.Lock()
err := tPlayer.Update().SetVacCount(int(bans[0].NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background())
tPlayer, err := db.Player.UpdateOneID(pS.SteamID).
SetName(pS.PersonaName).
SetAvatarURL(pS.LargeAvatarURL).
SetSteamUpdated(time.Now().UTC()).
SetVanityURL(strings.ToLower(pS.ProfileURL)).
SetVanityURLReal(pS.ProfileURL).
SetSteamUpdated(time.Now().UTC()).
SetProfileCreated(time.Unix(pS.TimeCreated, 0).UTC()).
Save(context.Background())
lock.Unlock()
if err != nil {
return nil, err
}
nPlayers = append(nPlayers, tPlayer)
}
return tPlayer, nil
rl.Take()
bans, err := steamapi.GetPlayerBans(idsToUpdate, apiKey)
if err != nil {
return nil, err
}
for _, ban := range bans {
if ban.NumberOfVACBans > 0 {
banDate := time.Now().UTC().AddDate(0, 0, -1*int(bans[0].DaysSinceLastBan))
lock.Lock()
err := db.Player.UpdateOneID(ban.SteamID).SetVacCount(int(ban.NumberOfVACBans)).SetVacDate(banDate).Exec(context.Background())
lock.Unlock()
if err != nil {
return nil, err
}
}
}
return nPlayers, nil
}