t2-stat-parser/app/t2-stats/parser.go
2020-03-07 09:47:28 -05:00

290 lines
7.4 KiB
Go

/*
Parser for DarkTiger's T2 Server Stats
[TODO]
- Read Additional GameTypes on the fly
- Use go modules
- Update to v4 SQL driver
- Ability to download stat files from remote server via FTP
*/
package parser
import (
"bufio"
"database/sql"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
_ "github.com/jackc/pgx/stdlib"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/rs/xid"
)
var (
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
db *sqlx.DB
maxStatOverwrite int = 100
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
)
func getenvWithDefault(name, defaultValue string) string {
val := os.Getenv(name)
if val == "" {
val = defaultValue
}
return val
}
func genXid() string {
id := xid.New()
return id.String()
}
// Game is...
type Game struct {
playerGUID int `db.players:"player_guid"`
playerName string `db.players:"player_name"`
totalGames int `db.players:"total_games"`
dbStatOverWrite int `db.players:"stat_overwrite"`
statOverWrite int
gameMap string `db.games:"map"`
gameType string `db.games:"gametype"`
dateStamp string `db.games:"datestamp"`
stats string `db.games:"stats"`
uuid string `db.games:"uuid"`
}
func init() {
start := time.Now()
flag.Parse()
var err error
// postgres connection
if *connectionString == "" {
log.Fatalln("Please pass the connection string using the -conn option")
}
db, err = sqlx.Connect("pgx", *connectionString)
if err != nil {
log.Fatalf("Unable to establish connection: %v\n", err)
}
// ----
var statFiles []string
statFolder := "serverStats/CTFGame" // GameType is CTFGame
fileErr := filepath.Walk(statFolder, func(path string, info os.FileInfo, fileErr error) error {
r, err := regexp.MatchString("g.cs", path)
if err == nil && r {
statFiles = append(statFiles, path)
}
return nil
})
if fileErr != nil {
panic(fileErr)
}
for _, file := range statFiles {
fmt.Println(file)
g := Game{}
// The file name is the player GUID so we need to store that in the object since its not part of the stat line
// Add playerGUID, gameType as part of the stat line
g.gameType = strings.Split(file, "/")[1]
g.playerGUID, err = strconv.Atoi(regexp.MustCompile("[0-9]+").FindAllString(file, -1)[0])
if err != nil {
fmt.Println("Couldn't convert playerGUID to an int", err)
}
file, err := os.Open(file)
//file, err := os.Open("serverStats/CTFGame/1g.cs")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Raw map for the stat lines in the file
mStatLine := make(map[string][]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
statScanLine := scanner.Text()
// Split the lines on %t
lineParts := strings.Split(statScanLine, "%t")
// The first token of the part is the key
statHeading := lineParts[0]
// Loop over the parts from the string.
for i := range lineParts {
if i > 0 {
mStatLine[statHeading] = append(mStatLine[statHeading], lineParts[i])
}
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
g.playerName = mStatLine["playerName"][0]
g.totalGames, err = strconv.Atoi(mStatLine["totalGames"][0])
if err != nil {
fmt.Println("Couldn't convert totalGames to an int", err)
}
g.statOverWrite, err = strconv.Atoi(mStatLine["statsOverWrite"][0])
if err != nil {
fmt.Println("Couldn't convert statOverWrite to an int", err)
}
if debugLevel >= 1 {
// These dont have a position offset
fmt.Println("playerGUID", g.playerGUID)
fmt.Println("GameType", g.gameType)
fmt.Println("Player Name", g.playerName)
fmt.Println("Stat Overwrite", g.statOverWrite)
fmt.Println("maxStatOverwrite", maxStatOverwrite)
}
checkPlayer(g)
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID)
statCron := 0
if g.statOverWrite < g.dbStatOverWrite {
statCron = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
} else {
statCron = g.statOverWrite - g.dbStatOverWrite
}
if debugLevel >= 1 {
fmt.Println("statCron", statCron)
}
for i := 1; i <= statCron; i++ {
arrPosition := i - 1
fmt.Println(arrPosition)
parseStatOverWriteLine(g, mStatLine, arrPosition)
}
fmt.Println("---")
}
// Close connection to DB
//defer conn.Close(context.Background())
fmt.Println("Total Execution time: ", time.Since(start))
}
func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition int) {
if debugLevel == 1 {
fmt.Println("Running fn parseStatOverWriteLine")
fmt.Println("playerGUID", g.playerGUID, "arrPosition", arrPosition)
}
cleanStatLine := make(map[string][]string)
for index, element := range mStatLine {
if len(element) > 1 {
cleanStatLine[index] = append(cleanStatLine[index], element[arrPosition])
}
}
jsonStats, err := json.Marshal(cleanStatLine)
if err != nil {
log.Fatal("Error parsing jsonStats", err)
}
g.stats = string(jsonStats)
g.dateStamp = mStatLine["dateStamp"][arrPosition]
g.uuid = genXid()
g.gameMap = cleanStatLine["map"][0]
if debugLevel >= 2 {
// log the game struct
fmt.Println(g)
}
// insert game stat
addPlayerGameStat(g)
}
func rowExists(query string, args ...interface{}) bool {
var exists bool
query = fmt.Sprintf("SELECT exists (%s)", query)
err := db.QueryRow(query, args...).Scan(&exists)
if err != nil && err != sql.ErrNoRows {
errors.Wrap(err, "error checking if row exists")
}
return exists
}
func checkPlayer(g Game) {
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
if !check {
createPlayer(genXid(), g)
} else {
fmt.Println("Player already exists", g.playerName)
}
}
func createPlayer(uuid string, g Game) {
fmt.Println("Creating new player", g.playerName)
_, err := db.Exec("insert into players(uuid, player_guid, player_name) values($1,$2,$3)", uuid, g.playerGUID, g.playerName)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
os.Exit(1)
}
}
func getDBStatOverWrite(playerGUID int) int {
var dbStatOverWrite, dbPlayerGUID int
// Used to compare ingested statOverWrite to known dbStatOverWrite
err := db.QueryRow("select player_guid, stat_overwrite from players where player_guid = $1", playerGUID).Scan(&dbPlayerGUID, &dbStatOverWrite)
if err != nil {
fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
os.Exit(1)
}
return dbStatOverWrite
}
func addPlayerGameStat(g Game) {
if debugLevel == 1 {
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite, "g.statOverWrite", g.statOverWrite)
}
if g.dbStatOverWrite != g.statOverWrite {
// Insert new stat line
fmt.Println("New stat line!", g.playerName)
sqlInsert := `insert into games(player_guid, player_name, stat_overwrite, map, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8)`
_, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.stats, g.dateStamp, g.uuid, g.gameType)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
os.Exit(1)
}
// Need to update a players statOverWrite after comparison
sqlUpdate := `UPDATE players SET stat_overwrite = $2, total_games = $3 WHERE player_guid = $1;`
_, err = db.Exec(sqlUpdate, g.playerGUID, g.statOverWrite, g.totalGames)
if err != nil {
panic(err)
}
} else {
fmt.Println("This stat line already exists")
}
}