VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Vote.cpp
Go to the documentation of this file.
3
13#include <engine/igameeventsystem.h>
14#include <google/protobuf/descriptor.h>
15#include <google/protobuf/message.h>
16#include <networksystem/inetworkmessages.h>
17#include <networksystem/netmessage.h>
18#include <string_view>
19#include <utility>
20
21namespace VoltMod
22{
23
24static constexpr std::string_view ControllerClass = "vote_controller";
25
26// A client takes F1/F2 only while an issue is active. The engine must never run this one, so
27// `vote` and `callvote` are blocked during a vote.
28static constexpr int YesNoIssueIndex = 2;
29static constexpr int NoIssue = -1;
30static constexpr int AllTeams = -1;
31
32// The vote_cast event's option numbers.
33static constexpr int YesOption = 0;
34static constexpr int NoOption = 1;
35
36// Names carry the message prefix: a bare "VoteFailed" also matches CCSUsrMsg_CallVoteFailed.
37static constexpr std::string_view VoteStartMessage = "CCSUsrMsg_VoteStart";
38static constexpr std::string_view VotePassMessage = "CCSUsrMsg_VotePass";
39static constexpr std::string_view VoteFailedMessage = "CCSUsrMsg_VoteFailed";
40
42{
43 return message ? message->ToPB<ProtoMessage>() : nullptr;
44}
45
46/** @ref ProtoField plus what a missing field costs here; see Engine/ProtoReflect.hpp for why the
47 * fields are reached by name at all. */
48static const ProtoFieldDescriptor* VoteField(ProtoMessage* message, std::string_view name)
49{
50 const auto* field = ProtoField(*message, name);
51 if (!field)
52 {
53 const auto* descriptor = message->GetDescriptor();
54 Log::Warn("Vote: {} has no field '{}'; the panel may render incomplete.",
55 descriptor ? descriptor->name() : "<unknown>", name);
56 }
57 return field;
58}
59
60static void SetInt(ProtoMessage* message, std::string_view name, int32_t value)
61{
62 if (const auto* field = VoteField(message, name))
63 {
64 message->GetReflection()->SetInt32(message, field, value);
65 }
66}
67
68static void SetBool(ProtoMessage* message, std::string_view name, bool value)
69{
70 if (const auto* field = VoteField(message, name))
71 {
72 message->GetReflection()->SetBool(message, field, value);
73 }
74}
75
76static void SetString(ProtoMessage* message, std::string_view name, const std::string& value)
77{
78 if (const auto* field = VoteField(message, name))
79 {
80 message->GetReflection()->SetString(message, field, value);
81 }
82}
83
86 : _interfaces(interfaces), _entities(entities), _players(players), _events(events), _scheduler(scheduler)
87{
88 _leaving = players.Disconnected += [this](Player& player) { DropVoter(player.Slot()); };
89}
90
91MultiRecipientFilter Vote::Recipients() const
92{
94 for (const Player* player : _players.All())
95 {
96 if (!player->IsBot())
97 {
98 filter.AddRecipient(player->Slot());
99 }
100 }
101 return filter;
102}
103
104Schema::CVoteController Vote::Controller()
105{
106 return Schema::CVoteController{_entities.Find(ControllerClass).Raw()};
107}
108
110{
111 if (_running || !request.Passed)
112 {
113 return false;
114 }
115
116 Ballot ballot{.Request = std::move(request)};
117 for (const Player* player : _players.All())
118 {
119 if (!player->IsBot() && IsValidSlot(player->Slot()))
120 {
121 ballot.Waiting[player->Slot()] = true;
122 ++ballot.Tally.Eligible;
123 }
124 }
125 if (ballot.Tally.Eligible == 0)
126 {
127 return false;
128 }
129
131 {
132 controller.SetPotentialVotes(ballot.Tally.Eligible);
133 controller.SetIsYesNoVote(true);
134 // The VoteStart recipients decide who may vote.
135 controller.SetOnlyTeamToVote(AllTeams);
136 controller.SetActiveIssueIndex(YesNoIssueIndex);
137 }
138
139 Ballot& running = _running.emplace(std::move(ballot));
140 PublishCounts(running.Tally);
141 SendStart(running);
142 running.Timeout = _scheduler.Delay(running.Request.DurationMs, [this] { Finish(VoteEndReason::TimeUp); });
143 return true;
144}
145
146bool Vote::TryCastBallot(int slot, std::string_view option)
147{
148 if (!_running)
149 {
150 return false;
151 }
152
153 Ballot& ballot = *_running;
154 if (!IsValidSlot(slot) || !ballot.Waiting[slot])
155 {
156 return true;
157 }
158
159 if (option == "option1")
160 {
161 ++ballot.Tally.Yes;
162 PublishBallot(slot, YesOption);
163 }
164 else if (option == "option2")
165 {
166 ++ballot.Tally.No;
167 PublishBallot(slot, NoOption);
168 }
169 else
170 {
171 return true;
172 }
173
174 ballot.Waiting[slot] = false;
175 PublishCounts(ballot.Tally);
176 CloseIfAllVoted();
177 return true;
178}
179
180void Vote::DropVoter(int slot)
181{
182 if (!_running || !IsValidSlot(slot) || !_running->Waiting[slot])
183 {
184 return;
185 }
186
187 // A player who leaves without voting would otherwise hold the vote open until it times out.
188 _running->Waiting[slot] = false;
189 --_running->Tally.Eligible;
190 PublishCounts(_running->Tally);
191 CloseIfAllVoted();
192}
193
194void Vote::CloseIfAllVoted()
195{
196 if (_running->Tally.Cast() < _running->Tally.Eligible)
197 {
198 return;
199 }
200
201 // Deferred a tick: the engine may still be inside the command dispatch.
202 _running->Close = _scheduler.NextTick([this] { Finish(VoteEndReason::AllVoted); });
203}
204
206{
207 if (_running)
208 {
209 Finish(reason);
210 }
211}
212
213void Vote::Finish(VoteEndReason reason)
214{
215 // Taken out first: the callbacks may start the next vote. Dropping it cancels both timers.
216 Ballot ballot = std::move(*_running);
217 _running.reset();
218
219 // A cancelled vote never asks the caller whether it passed.
220 const bool passed = reason != VoteEndReason::Cancelled && ballot.Request.Passed(ballot.Tally);
221
222 SendOutcome(ballot.Request, passed);
223
225 {
226 controller.SetActiveIssueIndex(NoIssue);
227 }
228
229 if (ballot.Request.Finished)
230 {
231 ballot.Request.Finished(passed, reason);
232 }
233}
234
235void Vote::PublishBallot(int slot, int option)
236{
237 // The voter's panel registers the key press from this event.
239 if (!event)
240 {
241 return;
242 }
243
244 event->SetInt("vote_option", option);
245 event->SetInt("team", AllTeams);
246 event->SetPlayer("userid", CPlayerSlot(slot));
247 _events.FireEvent(event);
248}
249
250void Vote::PublishCounts(const VoteTally& tally)
251{
252 // The panel reads its tally from this event.
254 if (!event)
255 {
256 return;
257 }
258
259 event->SetInt("vote_option1", tally.Yes);
260 event->SetInt("vote_option2", tally.No);
261 event->SetInt("vote_option3", 0);
262 event->SetInt("vote_option4", 0);
263 event->SetInt("vote_option5", 0);
264 event->SetInt("potentialVotes", tally.Eligible);
265 _events.FireEvent(event);
266}
267
268void Vote::SendStart(const Ballot& ballot)
269{
270 MultiRecipientFilter filter = Recipients();
271 PostUserMessage(_interfaces, _voteStartInternal, VoteStartMessage, filter, [&ballot](CNetMessage* raw) {
272 auto* start = AsProto(raw);
273 if (!start)
274 {
275 return false;
276 }
277 SetInt(start, "team", AllTeams);
278 SetInt(start, "player_slot", ballot.Request.Caller);
279 SetInt(start, "vote_type", -1);
280 SetString(start, "disp_str", ballot.Request.Title);
281 SetString(start, "details_str", ballot.Request.Detail);
282 SetBool(start, "is_yes_no_vote", true);
283 return true;
284 });
285}
286
287void Vote::SendOutcome(const VoteRequest& request, bool passed)
288{
289 // Pass and fail are distinct message types, so each gets its own cache slot.
290 auto& cached = passed ? _votePassInternal : _voteFailedInternal;
291 MultiRecipientFilter filter = Recipients();
292
295 auto* outcome = AsProto(raw);
296 if (!outcome)
297 {
298 return false;
299 }
300 SetInt(outcome, "team", AllTeams);
301 if (passed)
302 {
303 SetInt(outcome, "vote_type", -1);
304 SetString(outcome, "disp_str", request.Title);
305 SetString(outcome, "details_str", request.Detail);
306 }
307 else
308 {
309 SetInt(outcome, "reason", 0);
310 }
311 return true;
312 });
313}
314
315} // namespace VoltMod
Access CS user-message fields through protobuf reflection.
A player's identity behind a slot: name, money, team, scoreboard row. It survives death and respawn; ...
runtime.Entities: finds and creates entities. Everything it returns is valid for this frame only; see...
Entity Find(std::string_view className)
CEntityInstance * Raw() const noexcept
Definition Entity.hpp:47
Creates, fires and listens to game events.
bool FireEvent(IGameEvent *event, bool broadcast=true)
IGameEvent * CreateEvent(std::string_view name)
Routes a user message or filtered sound to a chosen set of player slots. Starts empty; callers add ea...
The roster: every connected player, and the signals for the connection lifecycle.
std::span< Player *const > All() const
One connected player, owned by PlayerManager for the length of the connection.
Definition Player.hpp:23
One-shot delays and repeating timers, run on the game thread.
Definition Scheduler.hpp:18
Subscription NextTick(std::function< void()> callback)
Definition Scheduler.cpp:30
Subscription Delay(int64_t delayMs, std::function< void()> callback)
Definition Scheduler.cpp:20
Vote(Interfaces &interfaces, EntitySystem &entities, PlayerManager &players, GameEvents &events, Scheduler &scheduler)
Definition Vote.cpp:84
bool TryCastBallot(int slot, std::string_view option)
Definition Vote.cpp:146
bool Start(VoteRequest request)
Definition Vote.cpp:109
void End(VoteEndReason reason)
Definition Vote.cpp:205
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
static constexpr int NoOption
Definition Vote.cpp:34
google::protobuf::Message ProtoMessage
static void SetBool(ProtoMessage *message, std::string_view name, bool value)
Definition Vote.cpp:68
static constexpr std::string_view ControllerClass
static constexpr std::string_view VoteFailedMessage
Definition Vote.cpp:39
static constexpr int YesNoIssueIndex
Definition Vote.cpp:28
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static constexpr std::string_view VotePassMessage
Definition Vote.cpp:38
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
VoteEndReason
Definition Vote.hpp:24
@ Cancelled
an admin (or a peer feature) called it off
@ AllVoted
every eligible player cast a ballot
static constexpr std::string_view VoteStartMessage
Definition Vote.cpp:37
static ProtoMessage * AsProto(CNetMessage *message)
Definition Vote.cpp:41
static const ProtoFieldDescriptor * VoteField(ProtoMessage *message, std::string_view name)
Definition Vote.cpp:48
static void SetString(ProtoMessage *message, std::string_view name, const std::string &value)
Definition Vote.cpp:76
static void SetInt(ProtoMessage *message, std::string_view name, int32_t value)
Definition Vote.cpp:60
google::protobuf::FieldDescriptor ProtoFieldDescriptor
const ProtoFieldDescriptor * ProtoField(const ProtoMessage &message, std::string_view name)
bool PostUserMessage(Interfaces &interfaces, INetworkMessageInternal *&cached, std::string_view partialName, IRecipientFilter &filter, const std::function< bool(CNetMessage *)> &fill)
static constexpr int NoIssue
Definition Vote.cpp:29
static constexpr int AllTeams
Definition Vote.cpp:30
static constexpr int YesOption
Definition Vote.cpp:33
Centralized holder for all HL2SDK interface pointers.
static constexpr std::string_view EventName
static constexpr std::string_view EventName