366 lines
7.2 KiB
Go
366 lines
7.2 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||
"github.com/hako/durafmt"
|
||
"github.com/pkg/errors"
|
||
)
|
||
|
||
type achievement uint8
|
||
|
||
const (
|
||
_ achievement = iota
|
||
achievementBestFeeder
|
||
achievementVeryLongOnline
|
||
achievementDeathless
|
||
achievementPeaceable
|
||
achievementMaxFrags
|
||
achievementMaxLevel
|
||
)
|
||
|
||
func getEmojiByAchievement(ach achievement) string {
|
||
switch ach {
|
||
case achievementBestFeeder:
|
||
return "\xF0\x9F\x91\xBB"
|
||
case achievementVeryLongOnline:
|
||
return "\xF0\x9F\xA4\xAA"
|
||
case achievementDeathless:
|
||
return "\xF0\x9F\x98\x8E"
|
||
case achievementPeaceable:
|
||
return "\xF0\x9F\x98\x87"
|
||
case achievementMaxFrags:
|
||
return "\xF0\x9F\x98\x88"
|
||
case achievementMaxLevel:
|
||
return "\xF0\x9F\xA7\x99\xE2\x80\x8D\xE2\x99\x82\xEF\xB8\x8F"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
var allAchievement = []achievement{
|
||
achievementBestFeeder,
|
||
achievementVeryLongOnline,
|
||
achievementDeathless,
|
||
achievementPeaceable,
|
||
achievementMaxFrags,
|
||
achievementMaxLevel,
|
||
}
|
||
|
||
type playerInfo struct {
|
||
playerID string
|
||
playerMessageID int
|
||
isOnline bool
|
||
achievements []achievement
|
||
messageText string
|
||
}
|
||
|
||
func (p *playerInfo) setOnline(v bool) *playerInfo {
|
||
p.isOnline = v
|
||
return p
|
||
}
|
||
|
||
const (
|
||
emojiUp = "\xE2\xAC\x86"
|
||
emojiDeaths = "\xF0\x9F\x92\x80"
|
||
emojiTime = "\xF0\x9F\x95\x90"
|
||
emojiGun = "\xF0\x9F\x94\xAB"
|
||
emojiCheck = "\xE2\x9C\x85"
|
||
)
|
||
|
||
func (p *playerInfo) updatePlayerInfo(ctx context.Context) error {
|
||
botApi := getBotApi(ctx)
|
||
conf := getConfig(ctx)
|
||
db := getDB(ctx)
|
||
|
||
lines := make([]string, 0, 10)
|
||
|
||
player, err := db.getPlayerByID(ctx, p.playerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
kills, err := db.getKillsByPlayerID(ctx, player.id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
access := "\xF0\x9F\x94\xB4 *offLine*"
|
||
if p.isOnline {
|
||
access = "\xF0\x9F\x9F\xA2 *onLine*"
|
||
}
|
||
|
||
level := "-"
|
||
if player.level >= 0 {
|
||
level = fmt.Sprintf("%d", player.level)
|
||
}
|
||
|
||
d := durafmt.Parse(player.onlineDuration).LimitFirstN(3)
|
||
|
||
units, err := durafmt.DefaultUnitsCoder.Decode("мес.:мес.,нед.:нед.,дн.:дн.,чс.:чс.,мин.:мин.,cек.:cек.,-:-,-:-")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
lines = append(lines, fmt.Sprintf("*%s* | %s", player.name, access))
|
||
lines = append(lines, fmt.Sprintf("%s Уровень: *%s*", emojiUp, level))
|
||
lines = append(lines, fmt.Sprintf("%s Смертей: *%d*", emojiDeaths, player.deaths))
|
||
lines = append(lines, fmt.Sprintf("%s Фрагов: *%d*", emojiGun, kills))
|
||
lines = append(lines, fmt.Sprintf("%s Время в игре: *%s*", emojiTime, d.Format(units)))
|
||
|
||
if !player.lastLogout.IsZero() && !p.isOnline {
|
||
lines = append(lines, fmt.Sprintf("%s Был в сети: *%s*", emojiCheck, player.lastLogout.Format("02 Jan 15:04")))
|
||
}
|
||
|
||
if len(p.achievements) > 0 {
|
||
lines = append(lines, "-----")
|
||
|
||
emojiAch := make([]string, 0, len(p.achievements))
|
||
|
||
for _, a := range p.achievements {
|
||
emojiAch = append(emojiAch, getEmojiByAchievement(a))
|
||
}
|
||
|
||
lines = append(lines, strings.Join(emojiAch, " "))
|
||
}
|
||
|
||
text := strings.Join(lines, "\n")
|
||
|
||
if text != p.messageText {
|
||
|
||
var c tgbotapi.Chattable
|
||
|
||
if p.playerMessageID == 0 {
|
||
mess := tgbotapi.NewMessage(conf.ChatID, text)
|
||
mess.ParseMode = tgbotapi.ModeMarkdown
|
||
|
||
c = mess
|
||
} else {
|
||
mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, text)
|
||
mess.ParseMode = tgbotapi.ModeMarkdown
|
||
|
||
c = mess
|
||
}
|
||
|
||
m, err := botApi.Send(c)
|
||
if err != nil {
|
||
return errors.Wrap(err, "Failed to send message")
|
||
}
|
||
|
||
p.playerMessageID = m.MessageID
|
||
p.messageText = text
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (p *playerInfo) deletePlayerInfo(ctx context.Context) error {
|
||
botApi := getBotApi(ctx)
|
||
conf := getConfig(ctx)
|
||
|
||
mess := tgbotapi.NewDeleteMessage(conf.ChatID, p.playerMessageID)
|
||
_, err := botApi.Send(mess)
|
||
if err != nil {
|
||
return errors.Wrap(err, "Failed to send message")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (p *playerInfo) setAchievement(ach achievement) {
|
||
for _, a := range p.achievements {
|
||
if a == ach {
|
||
return
|
||
}
|
||
}
|
||
|
||
p.achievements = append(p.achievements, ach)
|
||
}
|
||
|
||
func (p *playerInfo) unsetAchievement(ach achievement) {
|
||
for i, a := range p.achievements {
|
||
if a == ach {
|
||
p.achievements = append(p.achievements[:i], p.achievements[i+1:]...)
|
||
}
|
||
}
|
||
}
|
||
|
||
type playersBoard struct {
|
||
mx sync.Mutex
|
||
players []*playerInfo
|
||
}
|
||
|
||
func newPlayersBoard() *playersBoard {
|
||
return &playersBoard{
|
||
players: make([]*playerInfo, 0),
|
||
}
|
||
}
|
||
|
||
func (pb *playersBoard) load(ctx context.Context) error {
|
||
pb.mx.Lock()
|
||
defer pb.mx.Unlock()
|
||
|
||
db := getDB(ctx)
|
||
|
||
players, err := db.getPlayers(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
pb.players = make([]*playerInfo, 0, len(players))
|
||
|
||
for _, p := range players {
|
||
pi := &playerInfo{
|
||
playerID: p.id,
|
||
}
|
||
|
||
pb.players = append(pb.players, pi)
|
||
}
|
||
|
||
for _, a := range allAchievement {
|
||
err := pb.updateAchievements(ctx, a)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
for _, p := range pb.players {
|
||
err := p.updatePlayerInfo(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo {
|
||
for i := range pb.players {
|
||
if pb.players[i].playerID == playerID {
|
||
return pb.players[i]
|
||
}
|
||
}
|
||
|
||
pb.mx.Lock()
|
||
defer pb.mx.Unlock()
|
||
|
||
pb.players = append(pb.players, &playerInfo{
|
||
playerID: playerID,
|
||
})
|
||
|
||
return pb.players[len(pb.players)-1]
|
||
}
|
||
|
||
func (pb *playersBoard) updateAchievements(ctx context.Context, ach achievement) error {
|
||
db := getDB(ctx)
|
||
|
||
var value int
|
||
|
||
playerIDs := make([]string, 0, len(pb.players))
|
||
|
||
setValue := func(v int, playerID string) {
|
||
if value < v {
|
||
value = v
|
||
|
||
playerIDs = []string{playerID}
|
||
} else if value == v {
|
||
playerIDs = append(playerIDs, playerID)
|
||
}
|
||
}
|
||
|
||
for _, p := range pb.players {
|
||
player, err := db.getPlayerByID(ctx, p.playerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
switch ach {
|
||
case achievementBestFeeder:
|
||
setValue(player.deaths, p.playerID)
|
||
|
||
case achievementVeryLongOnline:
|
||
setValue(int(player.onlineDuration/time.Second), p.playerID)
|
||
|
||
case achievementDeathless:
|
||
if player.deaths == 0 {
|
||
playerIDs = append(playerIDs, p.playerID)
|
||
}
|
||
|
||
case achievementPeaceable:
|
||
kills, err := db.getKillsByPlayerID(ctx, p.playerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
setValue(int(^uint(0)>>1)-kills, p.playerID)
|
||
|
||
case achievementMaxFrags:
|
||
kills, err := db.getKillsByPlayerID(ctx, p.playerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
setValue(kills, p.playerID)
|
||
|
||
case achievementMaxLevel:
|
||
setValue(player.level, p.playerID)
|
||
}
|
||
}
|
||
|
||
mainLoop:
|
||
for _, p := range pb.players {
|
||
for _, pid := range playerIDs {
|
||
if p.playerID == pid {
|
||
p.setAchievement(ach)
|
||
continue mainLoop
|
||
}
|
||
}
|
||
|
||
p.unsetAchievement(ach)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (pb *playersBoard) update(ctx context.Context, as ...achievement) error {
|
||
pb.mx.Lock()
|
||
defer pb.mx.Unlock()
|
||
|
||
for _, a := range as {
|
||
err := pb.updateAchievements(ctx, a)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
for _, p := range pb.players {
|
||
err := p.updatePlayerInfo(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (pb *playersBoard) run(ctx context.Context) {
|
||
log := getLogger(ctx)
|
||
|
||
<-ctx.Done()
|
||
|
||
pb.mx.Lock()
|
||
defer pb.mx.Unlock()
|
||
|
||
for _, p := range pb.players {
|
||
err := p.deletePlayerInfo(ctx)
|
||
if err != nil {
|
||
log.Error(err)
|
||
}
|
||
}
|
||
}
|