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