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