-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathannounce.go
179 lines (144 loc) · 4.72 KB
/
announce.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package main
import (
"context"
"fmt"
"log/slog"
"slices"
"sort"
"strings"
"time"
"github.com/leighmacdonald/bd/rules"
"github.com/leighmacdonald/steamid/v4/steamid"
)
type kickRequest struct {
steamID steamid.SteamID
reason KickReason
}
// overwatch handles looking through the current player states and finding targets to attempt to perform action against.
// This mainly includes announcing their status to lobby/in-game chat, and trying to kick them.
//
// Players may also be queued for automatic kicks manually by the player when they initiate a kick request from the
// ui/api. These kicks are given first priority.
type overwatch struct {
state *gameState
rcon rconConnection
settings configManager
queued []kickRequest
}
func newOverwatch(settings configManager, rcon rconConnection, state *gameState) overwatch {
return overwatch{settings: settings, rcon: rcon, state: state}
}
func (bb *overwatch) start(ctx context.Context) {
timer := time.NewTicker(time.Second * 1)
for {
select {
case <-timer.C:
bb.update()
case <-ctx.Done():
return
}
}
}
// nextKickTarget searches for the next eligible target to initiate a vote kick against.
func (bb *overwatch) nextKickTarget() (PlayerState, bool) {
var validTargets []PlayerState
// Pull names from the manual queue first.
if len(bb.queued) > 0 {
player, errNotFound := bb.state.players.bySteamID(bb.queued[0].steamID)
if errNotFound != nil {
// They are not in the game anymore.
if len(bb.queued) > 1 {
bb.queued = slices.Delete(bb.queued, 0, 1)
} else {
bb.queued = bb.queued[1:]
}
}
validTargets = append(validTargets, player)
}
for _, player := range bb.state.players.current() {
if len(player.Matches) > 0 && !player.Whitelist {
validTargets = append(validTargets, player)
}
}
if len(validTargets) == 0 {
return PlayerState{}, false
}
// Find players we have not tried yet.
sort.Slice(validTargets, func(i, j int) bool {
return validTargets[i].KickAttemptCount < validTargets[j].KickAttemptCount
})
return validTargets[0], true
}
// announceMatch handles announcing after a match is triggered against a player.
func (bb *overwatch) announceMatch(ctx context.Context, player PlayerState, matches []rules.MatchResult) {
settings, errSettings := bb.settings.settings(ctx)
if errSettings != nil {
slog.Error("Failed to load settings", errAttr(errSettings))
return
}
if len(matches) == 0 {
return
}
if time.Since(player.AnnouncedGeneralLast) >= DurationAnnounceMatchTimeout {
msg := "Matched player"
if player.Whitelist {
msg = "Matched whitelisted player"
}
for _, match := range matches {
slog.Debug(msg,
slog.String("match_type", match.MatcherType),
slog.String("sid", player.SteamID.String()),
slog.String("name", player.Personaname),
slog.String("origin", match.Origin))
}
player.AnnouncedGeneralLast = time.Now()
bb.state.players.update(player)
}
if player.Whitelist {
return
}
if settings.ChatWarningsEnabled && time.Since(player.AnnouncedPartyLast) >= DurationAnnounceMatchTimeout {
// Don't spam friends, but eventually remind them if they manage to forget long enough
for _, match := range matches {
if errLog := bb.sendChat(ctx, ChatDestParty, "(%d) [%s] [%s] %s ", player.UserID, match.Origin, strings.Join(match.Attributes, ","), player.Personaname); errLog != nil {
slog.Error("Failed to send party log message", errAttr(errLog))
return
}
}
player.AnnouncedPartyLast = time.Now()
bb.state.players.update(player)
}
}
// sendChat is used to send chat messages to the various chat interfaces in game: say|say_team|say_party.
func (bb *overwatch) sendChat(ctx context.Context, destination ChatDest, format string, args ...any) error {
var cmd string
switch destination {
case ChatDestAll:
cmd = fmt.Sprintf("say %s", fmt.Sprintf(format, args...))
case ChatDestTeam:
cmd = fmt.Sprintf("say_team %s", fmt.Sprintf(format, args...))
case ChatDestParty:
cmd = fmt.Sprintf("say_party %s", fmt.Sprintf(format, args...))
default:
return fmt.Errorf("%w: %s", errInvalidChatType, destination)
}
resp, errExec := bb.rcon.exec(ctx, cmd, false)
if errExec != nil {
return errExec
}
slog.Debug(resp, slog.String("cmd", cmd))
return nil
}
func (bb *overwatch) update() {
}
func (bb *overwatch) kick(ctx context.Context, player PlayerState, reason KickReason) {
player.KickAttemptCount++
defer bb.state.players.update(player)
cmd := fmt.Sprintf("callvote kick \"%d %s\"", player.UserID, reason)
resp, errCallVote := bb.rcon.exec(ctx, cmd, false)
if errCallVote != nil {
slog.Error("Failed to call vote", slog.String("steam_id", player.SteamID.String()), errAttr(errCallVote))
return
}
slog.Debug("Kick response", slog.String("resp", resp))
}