1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "voting.h"
4
5#include <base/system.h>
6
7#include <engine/shared/config.h>
8#include <engine/textrender.h>
9
10#include <generated/protocol.h>
11
12#include <game/client/components/scoreboard.h>
13#include <game/client/components/sounds.h>
14#include <game/client/gameclient.h>
15#include <game/localization.h>
16
17void CVoting::ConCallvote(IConsole::IResult *pResult, void *pUserData)
18{
19 CVoting *pSelf = (CVoting *)pUserData;
20 pSelf->Callvote(pType: pResult->GetString(Index: 0), pValue: pResult->GetString(Index: 1), pReason: pResult->NumArguments() > 2 ? pResult->GetString(Index: 2) : "");
21}
22
23void CVoting::ConVote(IConsole::IResult *pResult, void *pUserData)
24{
25 CVoting *pSelf = (CVoting *)pUserData;
26 if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "yes") == 0)
27 pSelf->Vote(v: 1);
28 else if(str_comp_nocase(a: pResult->GetString(Index: 0), b: "no") == 0)
29 pSelf->Vote(v: -1);
30}
31
32void CVoting::Callvote(const char *pType, const char *pValue, const char *pReason)
33{
34 if(Client()->IsSixup())
35 {
36 protocol7::CNetMsg_Cl_CallVote Msg;
37 Msg.m_pType = pType;
38 Msg.m_pValue = pValue;
39 Msg.m_pReason = pReason;
40 Msg.m_Force = false;
41 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL, NoTranslate: true);
42 return;
43 }
44 CNetMsg_Cl_CallVote Msg = {.m_pType: nullptr};
45 Msg.m_pType = pType;
46 Msg.m_pValue = pValue;
47 Msg.m_pReason = pReason;
48 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
49}
50
51void CVoting::CallvoteSpectate(int ClientId, const char *pReason, bool ForceVote)
52{
53 if(ForceVote)
54 {
55 char aBuf[128];
56 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "set_team %d -1", ClientId);
57 Client()->Rcon(pLine: aBuf);
58 }
59 else
60 {
61 char aBuf[32];
62 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", ClientId);
63 Callvote(pType: "spectate", pValue: aBuf, pReason);
64 }
65}
66
67void CVoting::CallvoteKick(int ClientId, const char *pReason, bool ForceVote)
68{
69 if(ForceVote)
70 {
71 char aBuf[128];
72 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "force_vote kick %d %s", ClientId, pReason);
73 Client()->Rcon(pLine: aBuf);
74 }
75 else
76 {
77 char aBuf[32];
78 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", ClientId);
79 Callvote(pType: "kick", pValue: aBuf, pReason);
80 }
81}
82
83void CVoting::CallvoteOption(int OptionId, const char *pReason, bool ForceVote)
84{
85 CVoteOptionClient *pOption = m_pFirst;
86 while(pOption && OptionId >= 0)
87 {
88 if(OptionId == 0)
89 {
90 if(ForceVote)
91 {
92 char aBuf[128] = "force_vote option \"";
93 char *pDst = aBuf + str_length(str: aBuf);
94 str_escape(dst: &pDst, src: pOption->m_aDescription, end: aBuf + sizeof(aBuf));
95 str_append(dst&: aBuf, src: "\" \"");
96 pDst = aBuf + str_length(str: aBuf);
97 str_escape(dst: &pDst, src: pReason, end: aBuf + sizeof(aBuf));
98 str_append(dst&: aBuf, src: "\"");
99 Client()->Rcon(pLine: aBuf);
100 }
101 else
102 Callvote(pType: "option", pValue: pOption->m_aDescription, pReason);
103 break;
104 }
105
106 OptionId--;
107 pOption = pOption->m_pNext;
108 }
109}
110
111void CVoting::RemovevoteOption(int OptionId)
112{
113 CVoteOptionClient *pOption = m_pFirst;
114 while(pOption && OptionId >= 0)
115 {
116 if(OptionId == 0)
117 {
118 char aBuf[128] = "remove_vote \"";
119 char *pDst = aBuf + str_length(str: aBuf);
120 str_escape(dst: &pDst, src: pOption->m_aDescription, end: aBuf + sizeof(aBuf));
121 str_append(dst&: aBuf, src: "\"");
122 Client()->Rcon(pLine: aBuf);
123 break;
124 }
125
126 OptionId--;
127 pOption = pOption->m_pNext;
128 }
129}
130
131void CVoting::AddvoteOption(const char *pDescription, const char *pCommand)
132{
133 char aBuf[128] = "add_vote \"";
134 char *pDst = aBuf + str_length(str: aBuf);
135 str_escape(dst: &pDst, src: pDescription, end: aBuf + sizeof(aBuf));
136 str_append(dst&: aBuf, src: "\" \"");
137 pDst = aBuf + str_length(str: aBuf);
138 str_escape(dst: &pDst, src: pCommand, end: aBuf + sizeof(aBuf));
139 str_append(dst&: aBuf, src: "\"");
140 Client()->Rcon(pLine: aBuf);
141}
142
143void CVoting::Vote(int v)
144{
145 CNetMsg_Cl_Vote Msg = {.m_Vote: v};
146 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
147}
148
149int CVoting::SecondsLeft() const
150{
151 return (m_Closetime - time()) / time_freq();
152}
153
154CVoting::CVoting()
155{
156 ClearOptions();
157 OnReset();
158}
159
160void CVoting::AddOption(const char *pDescription)
161{
162 if(m_NumVoteOptions == MAX_VOTE_OPTIONS)
163 return;
164
165 CVoteOptionClient *pOption;
166 if(m_pRecycleFirst)
167 {
168 pOption = m_pRecycleFirst;
169 m_pRecycleFirst = m_pRecycleFirst->m_pNext;
170 if(m_pRecycleFirst)
171 m_pRecycleFirst->m_pPrev = nullptr;
172 else
173 m_pRecycleLast = nullptr;
174 }
175 else
176 pOption = m_Heap.Allocate<CVoteOptionClient>();
177
178 pOption->m_pNext = nullptr;
179 pOption->m_pPrev = m_pLast;
180 if(pOption->m_pPrev)
181 pOption->m_pPrev->m_pNext = pOption;
182 m_pLast = pOption;
183 if(!m_pFirst)
184 m_pFirst = pOption;
185
186 str_copy(dst&: pOption->m_aDescription, src: pDescription);
187 ++m_NumVoteOptions;
188}
189
190void CVoting::RemoveOption(const char *pDescription)
191{
192 for(CVoteOptionClient *pOption = m_pFirst; pOption; pOption = pOption->m_pNext)
193 {
194 if(str_comp(a: pOption->m_aDescription, b: pDescription) == 0)
195 {
196 // remove it from the list
197 if(m_pFirst == pOption)
198 m_pFirst = m_pFirst->m_pNext;
199 if(m_pLast == pOption)
200 m_pLast = m_pLast->m_pPrev;
201 if(pOption->m_pPrev)
202 pOption->m_pPrev->m_pNext = pOption->m_pNext;
203 if(pOption->m_pNext)
204 pOption->m_pNext->m_pPrev = pOption->m_pPrev;
205 --m_NumVoteOptions;
206
207 // add it to recycle list
208 pOption->m_pNext = nullptr;
209 pOption->m_pPrev = m_pRecycleLast;
210 if(pOption->m_pPrev)
211 pOption->m_pPrev->m_pNext = pOption;
212 m_pRecycleLast = pOption;
213 if(!m_pRecycleFirst)
214 m_pRecycleLast = pOption;
215
216 break;
217 }
218 }
219}
220
221void CVoting::ClearOptions()
222{
223 m_Heap.Reset();
224
225 m_NumVoteOptions = 0;
226 m_pFirst = nullptr;
227 m_pLast = nullptr;
228
229 m_pRecycleFirst = nullptr;
230 m_pRecycleLast = nullptr;
231}
232
233void CVoting::OnReset()
234{
235 m_Opentime = m_Closetime = 0;
236 m_aDescription[0] = '\0';
237 m_aReason[0] = '\0';
238 m_Yes = m_No = m_Pass = m_Total = 0;
239 m_Voted = 0;
240 m_ReceivingOptions = false;
241}
242
243void CVoting::OnConsoleInit()
244{
245 Console()->Register(pName: "callvote", pParams: "s['kick'|'spectate'|'option'] s[id|option text] ?r[reason]", Flags: CFGFLAG_CLIENT, pfnFunc: ConCallvote, pUser: this, pHelp: "Call vote");
246 Console()->Register(pName: "vote", pParams: "r['yes'|'no']", Flags: CFGFLAG_CLIENT, pfnFunc: ConVote, pUser: this, pHelp: "Vote yes/no");
247}
248
249void CVoting::OnMessage(int MsgType, void *pRawMsg)
250{
251 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
252 return;
253
254 if(MsgType == NETMSGTYPE_SV_VOTESET)
255 {
256 CNetMsg_Sv_VoteSet *pMsg = (CNetMsg_Sv_VoteSet *)pRawMsg;
257 OnReset();
258 if(pMsg->m_Timeout)
259 {
260 str_copy(dst&: m_aDescription, src: pMsg->m_pDescription);
261 str_copy(dst&: m_aReason, src: pMsg->m_pReason);
262 m_Opentime = time();
263 m_Closetime = time() + time_freq() * pMsg->m_Timeout;
264
265 if(Client()->RconAuthed())
266 {
267 char aBuf[512];
268 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", m_aDescription, m_aReason);
269 Client()->Notify(pTitle: "DDNet Vote", pMessage: aBuf);
270 GameClient()->m_Sounds.Play(Channel: CSounds::CHN_GUI, SetId: SOUND_CHAT_HIGHLIGHT, Volume: 1.0f);
271 }
272 }
273 }
274 else if(MsgType == NETMSGTYPE_SV_VOTESTATUS)
275 {
276 CNetMsg_Sv_VoteStatus *pMsg = (CNetMsg_Sv_VoteStatus *)pRawMsg;
277 m_Yes = pMsg->m_Yes;
278 m_No = pMsg->m_No;
279 m_Pass = pMsg->m_Pass;
280 m_Total = pMsg->m_Total;
281 }
282 else if(MsgType == NETMSGTYPE_SV_VOTECLEAROPTIONS)
283 {
284 ClearOptions();
285 }
286 else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONLISTADD)
287 {
288 CNetMsg_Sv_VoteOptionListAdd *pMsg = (CNetMsg_Sv_VoteOptionListAdd *)pRawMsg;
289 int NumOptions = pMsg->m_NumOptions;
290 for(int i = 0; i < NumOptions; ++i)
291 {
292 switch(i)
293 {
294 case 0: AddOption(pDescription: pMsg->m_pDescription0); break;
295 case 1: AddOption(pDescription: pMsg->m_pDescription1); break;
296 case 2: AddOption(pDescription: pMsg->m_pDescription2); break;
297 case 3: AddOption(pDescription: pMsg->m_pDescription3); break;
298 case 4: AddOption(pDescription: pMsg->m_pDescription4); break;
299 case 5: AddOption(pDescription: pMsg->m_pDescription5); break;
300 case 6: AddOption(pDescription: pMsg->m_pDescription6); break;
301 case 7: AddOption(pDescription: pMsg->m_pDescription7); break;
302 case 8: AddOption(pDescription: pMsg->m_pDescription8); break;
303 case 9: AddOption(pDescription: pMsg->m_pDescription9); break;
304 case 10: AddOption(pDescription: pMsg->m_pDescription10); break;
305 case 11: AddOption(pDescription: pMsg->m_pDescription11); break;
306 case 12: AddOption(pDescription: pMsg->m_pDescription12); break;
307 case 13: AddOption(pDescription: pMsg->m_pDescription13); break;
308 case 14: AddOption(pDescription: pMsg->m_pDescription14);
309 }
310 }
311 }
312 else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONADD)
313 {
314 CNetMsg_Sv_VoteOptionAdd *pMsg = (CNetMsg_Sv_VoteOptionAdd *)pRawMsg;
315 AddOption(pDescription: pMsg->m_pDescription);
316 }
317 else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONREMOVE)
318 {
319 CNetMsg_Sv_VoteOptionRemove *pMsg = (CNetMsg_Sv_VoteOptionRemove *)pRawMsg;
320 RemoveOption(pDescription: pMsg->m_pDescription);
321 }
322 else if(MsgType == NETMSGTYPE_SV_YOURVOTE)
323 {
324 CNetMsg_Sv_YourVote *pMsg = (CNetMsg_Sv_YourVote *)pRawMsg;
325 m_Voted = pMsg->m_Voted;
326 }
327 else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONGROUPSTART)
328 {
329 m_ReceivingOptions = true;
330 }
331 else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONGROUPEND)
332 {
333 m_ReceivingOptions = false;
334 }
335}
336
337void CVoting::Render()
338{
339 if((!g_Config.m_ClShowVotesAfterVoting && !GameClient()->m_Scoreboard.IsActive() && TakenChoice()) || !IsVoting())
340 return;
341 const int Seconds = SecondsLeft();
342 if(Seconds < 0)
343 {
344 OnReset();
345 return;
346 }
347
348 CUIRect View = {.x: 0.0f, .y: 60.0f, .w: 120.0f, .h: 38.0f};
349 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_R, Rounding: 3.0f);
350 View.Margin(Cut: 3.0f, pOtherRect: &View);
351
352 SLabelProperties Props;
353 Props.m_EllipsisAtEnd = true;
354
355 char aBuf[256];
356 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%ds left"), Seconds);
357
358 CUIRect Row, LeftColumn, RightColumn, ProgressSpinner;
359 View.HSplitTop(Cut: 6.0f, pTop: &Row, pBottom: &View);
360 Row.VSplitRight(Cut: TextRender()->TextWidth(Size: 6.0f, pText: aBuf), pLeft: &LeftColumn, pRight: &RightColumn);
361 LeftColumn.VSplitRight(Cut: 2.0f, pLeft: &LeftColumn, pRight: nullptr);
362 LeftColumn.VSplitRight(Cut: 6.0f, pLeft: &LeftColumn, pRight: &ProgressSpinner);
363 LeftColumn.VSplitRight(Cut: 2.0f, pLeft: &LeftColumn, pRight: nullptr);
364
365 SProgressSpinnerProperties ProgressProps;
366 ProgressProps.m_Progress = std::clamp(val: (time() - m_Opentime) / (float)(m_Closetime - m_Opentime), lo: 0.0f, hi: 1.0f);
367 Ui()->RenderProgressSpinner(Center: ProgressSpinner.Center(), OuterRadius: ProgressSpinner.h / 2.0f, Props: ProgressProps);
368
369 Ui()->DoLabel(pRect: &RightColumn, pText: aBuf, Size: 6.0f, Align: TEXTALIGN_MR);
370
371 Props.m_MaxWidth = LeftColumn.w;
372 Ui()->DoLabel(pRect: &LeftColumn, pText: VoteDescription(), Size: 6.0f, Align: TEXTALIGN_ML, LabelProps: Props);
373
374 View.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &View);
375 View.HSplitTop(Cut: 6.0f, pTop: &Row, pBottom: &View);
376 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %s", Localize(pStr: "Reason:"), VoteReason());
377 Props.m_MaxWidth = Row.w;
378 Ui()->DoLabel(pRect: &Row, pText: aBuf, Size: 6.0f, Align: TEXTALIGN_ML, LabelProps: Props);
379
380 View.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &View);
381 View.HSplitTop(Cut: 4.0f, pTop: &Row, pBottom: &View);
382 RenderBars(Bars: Row);
383
384 View.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &View);
385 View.HSplitTop(Cut: 6.0f, pTop: &Row, pBottom: &View);
386 Row.VSplitMid(pLeft: &LeftColumn, pRight: &RightColumn, Spacing: 4.0f);
387
388 char aKey[64];
389 GameClient()->m_Binds.GetKey(pBindStr: "vote yes", pBuf: aKey, BufSize: sizeof(aKey));
390 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s - %s", aKey, Localize(pStr: "Vote yes"));
391 TextRender()->TextColor(Color: TakenChoice() == 1 ? ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f) : TextRender()->DefaultTextColor());
392 Ui()->DoLabel(pRect: &LeftColumn, pText: aBuf, Size: 6.0f, Align: TEXTALIGN_ML);
393
394 GameClient()->m_Binds.GetKey(pBindStr: "vote no", pBuf: aKey, BufSize: sizeof(aKey));
395 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s - %s", Localize(pStr: "Vote no"), aKey);
396 TextRender()->TextColor(Color: TakenChoice() == -1 ? ColorRGBA(0.95f, 0.25f, 0.25f, 0.85f) : TextRender()->DefaultTextColor());
397 Ui()->DoLabel(pRect: &RightColumn, pText: aBuf, Size: 6.0f, Align: TEXTALIGN_MR);
398
399 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
400}
401
402void CVoting::RenderBars(CUIRect Bars) const
403{
404 Bars.Draw(Color: ColorRGBA(0.8f, 0.8f, 0.8f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: Bars.h / 2.0f);
405
406 CUIRect Splitter;
407 Bars.VMargin(Cut: (Bars.w - 2.0f) / 2.0f, pOtherRect: &Splitter);
408 Splitter.Draw(Color: ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
409
410 if(m_Total)
411 {
412 if(m_Yes)
413 {
414 CUIRect YesArea;
415 Bars.VSplitLeft(Cut: Bars.w * m_Yes / m_Total, pLeft: &YesArea, pRight: nullptr);
416 YesArea.Draw(Color: ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f), Corners: IGraphics::CORNER_ALL, Rounding: YesArea.h / 2.0f);
417 }
418
419 if(m_No)
420 {
421 CUIRect NoArea;
422 Bars.VSplitRight(Cut: Bars.w * m_No / m_Total, pLeft: nullptr, pRight: &NoArea);
423 NoArea.Draw(Color: ColorRGBA(0.9f, 0.2f, 0.2f, 0.85f), Corners: IGraphics::CORNER_ALL, Rounding: NoArea.h / 2.0f);
424 }
425 }
426}
427