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 | |
17 | void 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 | |
23 | void 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 | |
32 | void 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 | |
41 | void 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 | |
57 | void 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 | |
73 | void 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 | |
102 | void 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 | |
123 | void 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 | |
136 | void 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 | |
144 | int CVoting::SecondsLeft() const |
145 | { |
146 | return (m_Closetime - time()) / time_freq(); |
147 | } |
148 | |
149 | CVoting::CVoting() |
150 | { |
151 | ClearOptions(); |
152 | OnReset(); |
153 | } |
154 | |
155 | void 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 | |
185 | void 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 | |
216 | void 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 | |
228 | void 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 | |
238 | void 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 | |
244 | void 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 | |
329 | void 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 | |
394 | void 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 | |