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" ) 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 := "\xE2\x9D\x8C offLine" if p.isOnline { access = "\xE2\x9C\x85 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))) 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) } } }