Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for new stats parsing backend #592

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,6 @@ func serveCmd() *cobra.Command { //nolint:maintidx
appeals := appeal.NewAppealUsecase(appeal.NewAppealRepository(dbConn), banUsecase, personUsecase, notificationUsecase, configUsecase)

matchRepo := match.NewMatchRepository(eventBroadcaster, dbConn, personUsecase, serversUC, notificationUsecase, stateUsecase, weaponsMap)
go matchRepo.Start(ctx)

matchUsecase := match.NewMatchUsecase(matchRepo, stateUsecase, serversUC, notificationUsecase)

Expand Down
75 changes: 4 additions & 71 deletions internal/demo/demo_usecase.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
package demo

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/dustin/go-humanize"
"github.com/gin-gonic/gin"
"github.com/leighmacdonald/gbans/internal/domain"
"github.com/leighmacdonald/gbans/pkg/demoparse"
"github.com/leighmacdonald/gbans/pkg/fs"
"github.com/leighmacdonald/gbans/pkg/log"
"github.com/ricochet2200/go-disk-usage/du"
Expand Down Expand Up @@ -205,69 +201,6 @@ func (d demoUsecase) GetDemos(ctx context.Context) ([]domain.DemoFile, error) {
return d.repository.GetDemos(ctx)
}

func (d demoUsecase) SendAndParseDemo(ctx context.Context, path string) (*domain.DemoDetails, error) {
fileHandle, errDF := os.Open(path)
if errDF != nil {
return nil, errors.Join(errDF, domain.ErrDemoLoad)
}

content, errContent := io.ReadAll(fileHandle)
if errContent != nil {
return nil, errors.Join(errDF, domain.ErrDemoLoad)
}

info, errInfo := fileHandle.Stat()
if errInfo != nil {
return nil, errors.Join(errInfo, domain.ErrDemoLoad)
}

log.Closer(fileHandle)

body := new(bytes.Buffer)
writer := multipart.NewWriter(body)

part, errCreate := writer.CreateFormFile("file", info.Name())
if errCreate != nil {
return nil, errors.Join(errCreate, domain.ErrDemoLoad)
}

if _, err := part.Write(content); err != nil {
return nil, errors.Join(errCreate, domain.ErrDemoLoad)
}

if errClose := writer.Close(); errClose != nil {
return nil, errors.Join(errClose, domain.ErrDemoLoad)
}

req, errReq := http.NewRequestWithContext(ctx, http.MethodPost, d.config.Config().Demo.DemoParserURL, body)
if errReq != nil {
return nil, errors.Join(errReq, domain.ErrDemoLoad)
}
req.Header.Set("Content-Type", writer.FormDataContentType())

client := &http.Client{}
resp, errSend := client.Do(req)
if errSend != nil {
return nil, errors.Join(errSend, domain.ErrDemoLoad)
}

defer resp.Body.Close()

var demo domain.DemoDetails

// TODO remove this extra copy once this feature doesnt have much need for debugging/inspection.
rawBody, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return nil, errors.Join(errRead, domain.ErrDemoLoad)
}

if errDecode := json.NewDecoder(bytes.NewReader(rawBody)).Decode(&demo); errDecode != nil {
return nil, errors.Join(errDecode, domain.ErrDemoLoad)
}

return &demo, nil
}

func (d demoUsecase) CreateFromAsset(ctx context.Context, asset domain.Asset, serverID int) (*domain.DemoFile, error) {
_, errGetServer := d.servers.Server(ctx, serverID)
if errGetServer != nil {
Expand All @@ -290,13 +223,13 @@ func (d demoUsecase) CreateFromAsset(ctx context.Context, asset domain.Asset, se
// TODO change this data shape as we have not needed this in a long time. Only keys the are used.
intStats := map[string]gin.H{}

demoDetail, errDetail := d.SendAndParseDemo(ctx, asset.LocalPath)
demoDetail, errDetail := demoparse.Submit(ctx, d.config.Config().Demo.DemoParserURL, asset.LocalPath)
if errDetail != nil {
return nil, errDetail
}

for key := range demoDetail.State.Users {
intStats[key] = gin.H{}
intStats[strconv.Itoa(key)] = gin.H{}
}

timeStr := fmt.Sprintf("%s-%s", namePartsAll[0], namePartsAll[1])
Expand Down
9 changes: 6 additions & 3 deletions internal/discord/discord_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,12 +614,15 @@ func (h discordService) makeOnLogs() func(context.Context, *discordgo.Session, *

for _, match := range matches {
status := ":x:"
if match.IsWinner {
status = ":white_check_mark:"

for _, player := range match.Players {
if player.SteamID == author.SteamID && match.Winner == player.Team {
status = ":white_check_mark:"
}
}

_, _ = matchesWriter.WriteString(fmt.Sprintf("%s [%s](%s) `%s` `%s`\n",
status, match.Title, h.config.ExtURL(match), match.MapName, match.TimeStart.Format(time.DateOnly)))
status, match.Title, h.config.ExtURLRaw("/match/%s", match.MatchID.String()), match.MapName, match.TimeStart.Format(time.DateOnly)))
}

return LogsMessage(count, matchesWriter.String()), nil
Expand Down
33 changes: 2 additions & 31 deletions internal/domain/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type DemoUsecase interface {
GetDemos(ctx context.Context) ([]DemoFile, error)
CreateFromAsset(ctx context.Context, asset Asset, serverID int) (*DemoFile, error)
Cleanup(ctx context.Context)
SendAndParseDemo(ctx context.Context, path string) (*DemoDetails, error)
}

type DemoRepository interface {
Expand Down Expand Up @@ -54,38 +53,10 @@ type DemoFile struct {
AssetID uuid.UUID `json:"asset_id"`
}

const DemoType = "HL2DEMO"

type DemoInfo struct {
DemoID int64
Title string
AssetID uuid.UUID
}

type DemoPlayer struct {
Classes struct{} `json:"classes"`
Name string `json:"name"`
UserID int `json:"userId"` //nolint:tagliatelle
SteamID string `json:"steamId"` //nolint:tagliatelle
Team string `json:"team"`
}

type DemoHeader struct {
DemoType string `json:"demo_type"`
Version int `json:"version"`
Protocol int `json:"protocol"`
Server string `json:"server"`
Nick string `json:"nick"`
Map string `json:"map"`
Game string `json:"game"`
Duration float64 `json:"duration"`
Ticks int `json:"ticks"`
Frames int `json:"frames"`
Signon int `json:"signon"`
}

type DemoDetails struct {
State struct {
PlayerSummaries struct{} `json:"player_summaries"`
Users map[string]DemoPlayer `json:"users"`
} `json:"state"`
Header DemoHeader `json:"header"`
}
1 change: 0 additions & 1 deletion internal/domain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,4 @@ var (
ErrOpenFile = errors.New("could not open output file")
ErrFrontendRoutes = errors.New("failed to initialize frontend asset routes")
ErrPathInvalid = errors.New("invalid path specified")
ErrDemoLoad = errors.New("could not load demo file")
)
58 changes: 17 additions & 41 deletions internal/domain/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,15 @@ import (
"time"

"github.com/gofrs/uuid/v5"
"github.com/leighmacdonald/gbans/pkg/demoparse"
"github.com/leighmacdonald/gbans/pkg/fp"
"github.com/leighmacdonald/gbans/pkg/logparse"
"github.com/leighmacdonald/steamid/v4/steamid"
"golang.org/x/exp/slices"
)

type MatchTriggerType int

const (
MatchTriggerStart MatchTriggerType = 1
MatchTriggerEnd MatchTriggerType = 2
)

type MatchTrigger struct {
Type MatchTriggerType
UUID uuid.UUID
Server Server
MapName string
DemoName string
}

type MatchRepository interface {
Start(ctx context.Context)
StartMatch(startTrigger MatchTrigger)
EndMatch(endTrigger MatchTrigger)
Matches(ctx context.Context, opts MatchesQueryOpts) ([]MatchSummary, int64, error)
Matches(ctx context.Context, opts MatchesQueryOpts) ([]MatchResult, int64, error)
MatchGetByID(ctx context.Context, matchID uuid.UUID, match *MatchResult) error
MatchSave(ctx context.Context, match *logparse.Match, weaponMap fp.MutexMap[logparse.Weapon, int]) error
StatsPlayerClass(ctx context.Context, sid64 steamid.SteamID) (PlayerClassStatsCollection, error)
Expand All @@ -56,10 +39,9 @@ type MatchRepository interface {
GetMatchIDFromServerID(serverID int) (uuid.UUID, bool)
}
type MatchUsecase interface {
StartMatch(server Server, mapName string, demoName string) (uuid.UUID, error)
EndMatch(ctx context.Context, serverID int) (uuid.UUID, error)
CreateFromDemo(ctx context.Context, serverID int, details demoparse.Demo) (MatchResult, error)
GetMatchIDFromServerID(serverID int) (uuid.UUID, bool)
Matches(ctx context.Context, opts MatchesQueryOpts) ([]MatchSummary, int64, error)
Matches(ctx context.Context, opts MatchesQueryOpts) ([]MatchResult, int64, error)
MatchGetByID(ctx context.Context, matchID uuid.UUID, match *MatchResult) error
MatchSave(ctx context.Context, match *logparse.Match, weaponMap fp.MutexMap[logparse.Weapon, int]) error
StatsPlayerClass(ctx context.Context, sid64 steamid.SteamID) (PlayerClassStatsCollection, error)
Expand Down Expand Up @@ -186,6 +168,13 @@ type MatchWeapon struct {
MatchPlayerID int64 `json:"match_player_id"`
}

type MatchChat struct {
SteamID steamid.SteamID `json:"steam_id"`
PersonaName string `json:"persona_name"`
Body string `json:"body"`
Team bool `json:"team"`
}

type MatchResult struct {
MatchID uuid.UUID `json:"match_id"`
ServerID int `json:"server_id"`
Expand All @@ -196,7 +185,11 @@ type MatchResult struct {
TimeEnd time.Time `json:"time_end"`
Winner logparse.Team `json:"winner"`
Players []*MatchPlayer `json:"players"`
Chat PersonMessages `json:"chat"`
Chat []MatchChat `json:"chat"`
}

func (match *MatchResult) Path() string {
return "/log/" + match.MatchID.String()
}

func (match *MatchResult) TopPlayers() []*MatchPlayer {
Expand Down Expand Up @@ -443,7 +436,7 @@ type PlayerMedicStats struct {
type CommonPlayerStats struct {
SteamID steamid.SteamID `json:"steam_id"`
Name string `json:"name"`
AvatarHash string `json:"avatar_hash"`
AvatarHash string `json:"avatar_hash"` // todo make
Kills int `json:"kills"`
Assists int `json:"assists"`
Deaths int `json:"deaths"`
Expand Down Expand Up @@ -474,20 +467,3 @@ type PlayerStats struct {
MatchesWon int `json:"matches_won"`
PlayTime time.Duration `json:"play_time"`
}

type MatchSummary struct {
MatchID uuid.UUID `json:"match_id"`
ServerID int `json:"server_id"`
IsWinner bool `json:"is_winner"`
ShortName string `json:"short_name"`
Title string `json:"title"`
MapName string `json:"map_name"`
ScoreBlu int `json:"score_blu"`
ScoreRed int `json:"score_red"`
TimeStart time.Time `json:"time_start"`
TimeEnd time.Time `json:"time_end"`
}

func (m MatchSummary) Path() string {
return "/log/" + m.MatchID.String()
}
Loading
Loading