Skip to content

Commit

Permalink
Merge pull request #141 from Cufee/session-v2
Browse files Browse the repository at this point in the history
Session rendering v2
  • Loading branch information
Cufee authored Jan 20, 2025
2 parents 6877c89 + 485d04c commit f5ed5f0
Show file tree
Hide file tree
Showing 18 changed files with 1,139 additions and 31 deletions.
2 changes: 1 addition & 1 deletion docker-compose.dokploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
- docker-volume-backup.stop-during-backup=true # https://hub.docker.com/r/offen/docker-volume-backup
depends_on:
aftermath-migrate:
condition: service_started
condition: service_completed_successfully

networks:
dokploy-network:
Expand Down
Empty file.
Empty file.
2 changes: 0 additions & 2 deletions internal/stats/client/v1/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
)

func (c *client) EmptySessionCards(ctx context.Context, accountId string) (prepare.Cards, options.Metadata, error) {

meta := options.Metadata{Stats: make(map[string]fetch.AccountStatsOverPeriod)}

stop := meta.Timer("database#GetAccountByID")
Expand Down Expand Up @@ -46,7 +45,6 @@ func (c *client) EmptySessionCards(ctx context.Context, accountId string) (prepa
}

return cards, meta, nil

}

func (c *client) SessionCards(ctx context.Context, accountId string, from time.Time, o ...options.RequestOption) (prepare.Cards, options.Metadata, error) {
Expand Down
143 changes: 137 additions & 6 deletions internal/stats/client/v2/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,149 @@ package client

import (
"context"
"errors"
"slices"
"time"

"github.com/cufee/aftermath/internal/database"
"github.com/cufee/aftermath/internal/localization"
"github.com/cufee/aftermath/internal/stats/client/common"
"github.com/cufee/aftermath/internal/stats/fetch/v1"
prepare "github.com/cufee/aftermath/internal/stats/prepare/common/v1"
"github.com/cufee/aftermath/internal/stats/prepare/session/v1"
render "github.com/cufee/aftermath/internal/stats/render/session/v2"
)

func (r *client) SessionCards(ctx context.Context, accountId string, from time.Time, opts ...common.RequestOption) (session.Cards, common.Metadata, error) {
return r.v1.SessionCards(ctx, accountId, from, opts...)
func (c *client) SessionCards(ctx context.Context, accountId string, from time.Time, o ...common.RequestOption) (session.Cards, common.Metadata, error) {
opts := common.RequestOptions(o).Options()

meta := common.Metadata{Stats: make(map[string]fetch.AccountStatsOverPeriod)}

stop := meta.Timer("database#GetAccountByID")
_, err := c.database.GetAccountByID(ctx, accountId)
stop()
if database.IsNotFound(err) {
// record a session in the background
go recordAccountSnapshots(c.wargaming, c.database, accountId, opts.ReferenceID())

return session.Cards{}, meta, common.ErrAccountNotTracked
}
if err != nil {
return session.Cards{}, meta, err
}

printer, err := localization.NewPrinterWithFallback("stats", c.locale)
if err != nil {
return session.Cards{}, meta, err
}

stop = meta.Timer("fetchClient#SessionStats")
if from.IsZero() {
from = time.Now()
}
sessionStats, careerStats, err := c.fetchClient.SessionStats(ctx, accountId, from, opts.FetchOpts()...)
stop()
if err != nil {
if errors.Is(err, fetch.ErrSessionNotFound) {
go recordAccountSnapshots(c.wargaming, c.database, accountId, opts.ReferenceID())
}
return session.Cards{}, meta, err
}
meta.Stats["career"] = careerStats
meta.Stats["session"] = sessionStats

stop = meta.Timer("prepare#GetVehicles")
var vehicles []string
for id := range sessionStats.RegularBattles.Vehicles {
vehicles = append(vehicles, id)
}
for id := range sessionStats.RatingBattles.Vehicles {
if !slices.Contains(vehicles, id) {
vehicles = append(vehicles, id)
}
}
for id := range careerStats.RegularBattles.Vehicles {
if !slices.Contains(vehicles, id) {
vehicles = append(vehicles, id)
}
}
for id := range careerStats.RatingBattles.Vehicles {
if !slices.Contains(vehicles, id) {
vehicles = append(vehicles, id)
}
}
if opts.VehicleID() != "" && !slices.Contains(vehicles, opts.VehicleID()) {
vehicles = append(vehicles, opts.VehicleID())
}

glossary, err := c.database.GetVehicles(ctx, vehicles)
if err != nil {
return session.Cards{}, meta, err
}
stop()

stop = meta.Timer("prepare#NewCards")

cards, err := session.NewCards(sessionStats, careerStats, glossary, opts.PrepareOpts(printer, c.locale)...)
stop()
if err != nil {
return session.Cards{}, meta, err
}

return cards, meta, nil
}
func (r *client) SessionImage(ctx context.Context, accountId string, from time.Time, opts ...common.RequestOption) (common.Image, common.Metadata, error) {
return r.v1.SessionImage(ctx, accountId, from, opts...)

func (c *client) SessionImage(ctx context.Context, accountId string, from time.Time, o ...common.RequestOption) (common.Image, common.Metadata, error) {
opts := common.RequestOptions(o).Options()

cards, meta, err := c.SessionCards(ctx, accountId, from, o...)
if err != nil {
return nil, meta, err
}

printer, err := localization.NewPrinterWithFallback("stats", c.locale)
if err != nil {
return nil, meta, err
}

stop := meta.Timer("render#CardsToImage")
image, err := render.CardsToImage(meta.Stats["session"], meta.Stats["career"], cards, opts.Subscriptions, opts.RenderOpts(printer)...)
stop()
if err != nil {
return nil, meta, err
}

return &imageImp{image}, meta, err
}
func (r *client) EmptySessionCards(ctx context.Context, accountId string) (session.Cards, common.Metadata, error) {
return r.v1.EmptySessionCards(ctx, accountId)

func (c *client) EmptySessionCards(ctx context.Context, accountId string) (session.Cards, common.Metadata, error) {
meta := common.Metadata{Stats: make(map[string]fetch.AccountStatsOverPeriod)}

stop := meta.Timer("database#GetAccountByID")
account, err := c.database.GetAccountByID(ctx, accountId)
stop()
if err != nil {
if database.IsNotFound(err) {
_, err := c.fetchClient.Account(ctx, accountId) // this will cache the account
if err != nil {
return session.Cards{}, meta, err
}
return session.Cards{}, meta, common.ErrAccountNotTracked
}
return session.Cards{}, meta, err
}

printer, err := localization.NewPrinterWithFallback("stats", c.locale)
if err != nil {
return session.Cards{}, meta, err
}

stop = meta.Timer("prepare#NewCards")
cards, err := session.NewCards(fetch.AccountStatsOverPeriod{Account: account}, fetch.AccountStatsOverPeriod{Account: account}, nil, prepare.WithPrinter(printer, c.locale))
stop()
if err != nil {
return session.Cards{}, meta, err
}

return cards, meta, nil
}
22 changes: 0 additions & 22 deletions internal/stats/render/period/v2/overview.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,3 @@ func newOverviewRatingBlock(blockStyle blockStyle, block prepare.StatsBlock[peri
block.Label = common.GetRatingTierName(block.Value().Float())
return newOverviewBlockWithIcon(blockStyle, block, icon)
}

// func newHighlightCard(style highlightStyle, card period.VehicleCard) common.Block {
// titleBlock :=
// common.NewBlocksContent(common.Style{
// Direction: common.DirectionVertical,
// },
// common.NewTextContent(style.cardTitle, card.Meta),
// common.NewTextContent(style.tankName, card.Title),
// )

// var contentRow []common.Block
// for _, block := range card.Blocks {
// contentRow = append(contentRow, common.NewBlocksContent(common.Style{Direction: common.DirectionVertical, AlignItems: common.AlignItemsCenter},
// common.NewTextContent(style.blockValue, block.Value().String()),
// common.NewTextContent(style.blockLabel, block.Label),
// ))
// }

// return common.NewBlocksContent(style.container, titleBlock, common.NewBlocksContent(common.Style{
// Gap: style.container.Gap,
// }, contentRow...))
// }
38 changes: 38 additions & 0 deletions internal/stats/render/session/v2/background.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package session

import (
"image"
"image/color"
"slices"
"time"

"github.com/cufee/aftermath/internal/render/common"
"github.com/cufee/aftermath/internal/stats/frame"
)

func addBackgroundBranding(background image.Image, vehicles map[string]frame.VehicleStatsFrame, patternSeed int) image.Image {
var values []vehicleWN8
for _, vehicle := range vehicles {
if wn8 := vehicle.WN8(); !frame.InvalidValue.Equals(wn8) {
values = append(values, vehicleWN8{vehicle.VehicleID, wn8, int(vehicle.LastBattleTime.Unix())})
}
}
slices.SortFunc(values, func(a, b vehicleWN8) int { return b.sortKey - a.sortKey })
if len(values) >= 10 {
values = values[:9]
}

var accentColors []color.Color
for _, value := range values {
c := common.GetWN8Colors(value.wn8.Float()).Background
if _, _, _, a := c.RGBA(); a > 0 {
accentColors = append(accentColors, c)
}
}

if patternSeed == 0 {
patternSeed = int(time.Now().Unix())
}

return common.AddDefaultBrandedOverlay(background, accentColors, patternSeed, 0.5)
}
144 changes: 144 additions & 0 deletions internal/stats/render/session/v2/cards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package session

import (
"strconv"

prepare "github.com/cufee/aftermath/internal/stats/prepare/common/v1"
"github.com/cufee/aftermath/internal/stats/prepare/session/v1"
"github.com/cufee/facepaint/style"
"github.com/nao1215/imaging"

"github.com/cufee/aftermath/internal/database/models"
"github.com/cufee/aftermath/internal/render/common"
"github.com/cufee/aftermath/internal/stats/fetch/v1"
"github.com/cufee/facepaint"
)

func generateCards(session, career fetch.AccountStatsOverPeriod, cards session.Cards, _ []models.UserSubscription, opts common.Options) (*facepaint.Block, error) {
var (
renderUnratedVehiclesCount = 3 // minimum number of vehicle cards
// primary cards
// when there are some unrated battles or no battles at all
shouldRenderUnratedOverview = session.RegularBattles.Battles > 0 || session.RatingBattles.Battles < 1
// when there are 3 vehicle cards and no rating overview cards or there are 6 vehicle cards and some rating battles
shouldRenderUnratedHighlights = (session.RegularBattles.Battles > 0 && session.RatingBattles.Battles < 1 && len(cards.Unrated.Vehicles) > renderUnratedVehiclesCount) ||
(session.RegularBattles.Battles > 0 && len(cards.Unrated.Vehicles) > 3)
shouldRenderRatingOverview = session.RatingBattles.Battles > 0 && opts.VehicleID == ""
// secondary cards
shouldRenderUnratedVehicles = session.RegularBattles.Battles > 0 && len(cards.Unrated.Vehicles) > 0
)

// try to make the columns height roughly similar to primary column
if shouldRenderUnratedHighlights {
renderUnratedVehiclesCount += len(cards.Unrated.Highlights)
}
if shouldRenderRatingOverview {
renderUnratedVehiclesCount += 1
}

// calculate max overview block width to make all blocks the same size
var maxWidthOverviewColumn = make(map[string]float64)
for _, column := range cards.Unrated.Overview.Blocks {
for _, block := range column.Blocks {
switch block.Tag {
case prepare.TagWN8:
block.Label = common.GetWN8TierName(block.Value().Float())
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], iconSizeWN8)
}
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], facepaint.MeasureString(block.Label, styledOverviewCard.styleBlock(block).label.Font).TotalWidth)
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], facepaint.MeasureString(block.Value().String(), styledOverviewCard.styleBlock(block).value.Font).TotalWidth)
}
}
for _, column := range cards.Rating.Overview.Blocks {
for _, block := range column.Blocks {
switch block.Tag {
case prepare.TagRankedRating:
block.Label = common.GetRatingTierName(block.Value().Float())
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], iconSizeRating)
}
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], facepaint.MeasureString(block.Label, styledOverviewCard.styleBlock(block).label.Font).TotalWidth)
maxWidthOverviewColumn[string(column.Flavor)] = max(maxWidthOverviewColumn[string(column.Flavor)], facepaint.MeasureString(block.Value().String(), styledOverviewCard.styleBlock(block).value.Font).TotalWidth)
}
}

// calculate per block type width of highlight stats to make things even
var highlightBlockWidth = make(map[prepare.Tag]float64)
for _, highlight := range cards.Unrated.Highlights {
for _, block := range highlight.Blocks {
label := facepaint.MeasureString(block.Label, styledHighlightCard.blockLabel().Font).TotalWidth
value := facepaint.MeasureString(block.Value().String(), styledHighlightCard.blockValue().Font).TotalWidth
highlightBlockWidth[block.Tag] = max(highlightBlockWidth[block.Tag], label, value)
}
}

// calculate per block type width of vehicle stats to make things even
var vehicleBlockWidth = make(map[prepare.Tag]float64)
for _, card := range cards.Unrated.Vehicles {
for _, block := range card.Blocks {
labelStyle := styledVehicleLegendPill()
label := facepaint.MeasureString(block.Label, labelStyle.Font).TotalWidth + labelStyle.PaddingLeft + labelStyle.PaddingRight
value := facepaint.MeasureString(block.Value().String(), styledVehicleCard.value(0).Font).TotalWidth
vehicleBlockWidth[block.Tag] = max(vehicleBlockWidth[block.Tag], label, value)
}
}

var overviewCards = []*facepaint.Block{newPlayerNameCard(career.Account)}
// unrated overview
if shouldRenderUnratedOverview {
if card := newUnratedOverviewCard(cards.Unrated.Overview, maxWidthOverviewColumn); card != nil {
overviewCards = append(overviewCards, card)
}
}
// rating battles
if shouldRenderRatingOverview {
if card := newRatingOverviewCard(cards.Rating, maxWidthOverviewColumn); card != nil {
overviewCards = append(overviewCards, card)
}
}
// highlights
if shouldRenderUnratedHighlights {
for _, card := range cards.Unrated.Highlights {
overviewCards = append(overviewCards, newHighlightCard(card, highlightBlockWidth))
}
}

// vehicles
var vehicleCards []*facepaint.Block
if shouldRenderUnratedVehicles {
for i, card := range cards.Unrated.Vehicles {
if i == renderUnratedVehiclesCount {
break
}
vehicleCards = append(vehicleCards, newVehicleCard(card, vehicleBlockWidth))
}
}

var sectionBlocks []*facepaint.Block
sectionBlocks = append(sectionBlocks, facepaint.NewBlocksContent(style.NewStyle(style.Parent(styledCardsSection)), overviewCards...))
if len(vehicleCards) > 0 {
vehicleCards = append(vehicleCards, newVehicleLegendCard(cards.Unrated.Vehicles[0], vehicleBlockWidth))
sectionBlocks = append(sectionBlocks, facepaint.NewBlocksContent(style.NewStyle(style.Parent(styledCardsSection)), vehicleCards...))
}
statsCardsBlock := facepaint.NewBlocksContent(style.NewStyle(style.Parent(styledCardsSectionsWrapper)), sectionBlocks...)

cardsFrame := facepaint.NewBlocksContent(style.NewStyle(style.Parent(styledStatsFrame)), statsCardsBlock)

// resize and place background
if opts.Background != nil {
cardsFrameSize := cardsFrame.Dimensions()
opts.Background = imaging.Fill(opts.Background, cardsFrameSize.Width, cardsFrameSize.Height, imaging.Center, imaging.Lanczos)
if !opts.BackgroundIsCustom {
seed, _ := strconv.Atoi(career.Account.ID)
opts.Background = addBackgroundBranding(opts.Background, session.RegularBattles.Vehicles, seed)
}
cardsFrame = facepaint.NewBlocksContent(style.NewStyle(),
facepaint.MustNewImageContent(styledCardsBackground, opts.Background), cardsFrame,
)
}

var frameCards []*facepaint.Block
frameCards = append(frameCards, cardsFrame)
frameCards = append(frameCards, newFooterCard(session, cards, opts))

return facepaint.NewBlocksContent(style.NewStyle(style.Parent(styledFinalFrame)), frameCards...), nil
}
Loading

0 comments on commit f5ed5f0

Please sign in to comment.