| 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 | |
| 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 | 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 | |
| 51 | void 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 | |
| 67 | void 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 | |
| 83 | void 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 | |
| 111 | void 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 | |
| 131 | void 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 | |
| 143 | void CVoting::Vote(int v) |
| 144 | { |
| 145 | CNetMsg_Cl_Vote Msg = {.m_Vote: v}; |
| 146 | Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL); |
| 147 | } |
| 148 | |
| 149 | int CVoting::SecondsLeft() const |
| 150 | { |
| 151 | return (m_Closetime - time()) / time_freq(); |
| 152 | } |
| 153 | |
| 154 | CVoting::CVoting() |
| 155 | { |
| 156 | ClearOptions(); |
| 157 | OnReset(); |
| 158 | } |
| 159 | |
| 160 | void 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 | |
| 190 | void 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 | |
| 221 | void 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 | |
| 233 | void 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 | |
| 243 | void 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 | |
| 249 | void 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 | |
| 337 | void 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 | |
| 402 | void 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 | |