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 "binds.h"
4
5#include <base/log.h>
6#include <base/system.h>
7
8#include <engine/config.h>
9#include <engine/console.h>
10#include <engine/shared/config.h>
11
12#include <game/client/components/chat.h>
13#include <game/client/components/console.h>
14#include <game/client/gameclient.h>
15
16static constexpr LOG_COLOR BIND_PRINT_COLOR{.r: 255, .g: 255, .b: 204};
17
18bool CBinds::CBindsSpecial::OnInput(const IInput::CEvent &Event)
19{
20 if((Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_RELEASE)) == 0)
21 return false;
22
23 // only handle F and composed F binds
24 // do not handle F5 bind while menu is active
25 if(((Event.m_Key >= KEY_F1 && Event.m_Key <= KEY_F12) || (Event.m_Key >= KEY_F13 && Event.m_Key <= KEY_F24)) &&
26 (Event.m_Key != KEY_F5 || !GameClient()->m_Menus.IsActive()))
27 {
28 return m_pBinds->OnInput(Event);
29 }
30
31 return false;
32}
33
34CBinds::CBinds()
35{
36 mem_zero(block: m_aapKeyBindings, size: sizeof(m_aapKeyBindings));
37 m_SpecialBinds.m_pBinds = this;
38}
39
40CBinds::~CBinds()
41{
42 UnbindAll();
43}
44
45void CBinds::Bind(int KeyId, const char *pStr, bool FreeOnly, int ModifierCombination)
46{
47 dbg_assert(KeyId >= KEY_FIRST && KeyId < KEY_LAST, "KeyId invalid");
48 dbg_assert(ModifierCombination >= KeyModifier::NONE && ModifierCombination < KeyModifier::COMBINATION_COUNT, "ModifierCombination invalid");
49
50 if(FreeOnly && Get(KeyId, ModifierCombination)[0])
51 return;
52
53 free(ptr: m_aapKeyBindings[ModifierCombination][KeyId]);
54 m_aapKeyBindings[ModifierCombination][KeyId] = nullptr;
55
56 char aBindName[128];
57 GetKeyBindName(Key: KeyId, ModifierMask: ModifierCombination, pBuf: aBindName, BufSize: sizeof(aBindName));
58 if(!pStr[0])
59 {
60 log_info_color(BIND_PRINT_COLOR, "binds", "unbound %s", aBindName);
61 }
62 else
63 {
64 int Size = str_length(str: pStr) + 1;
65 m_aapKeyBindings[ModifierCombination][KeyId] = (char *)malloc(size: Size);
66 str_copy(dst: m_aapKeyBindings[ModifierCombination][KeyId], src: pStr, dst_size: Size);
67 log_info_color(BIND_PRINT_COLOR, "binds", "bound %s = %s", aBindName, m_aapKeyBindings[ModifierCombination][KeyId]);
68 }
69}
70
71int CBinds::GetModifierMask(IInput *pInput)
72{
73 int Mask = 0;
74 static const auto s_aModifierKeys = {
75 KEY_LSHIFT,
76 KEY_RSHIFT,
77 KEY_LCTRL,
78 KEY_RCTRL,
79 KEY_LALT,
80 KEY_RALT,
81 KEY_LGUI,
82 KEY_RGUI,
83 };
84 for(const auto Key : s_aModifierKeys)
85 {
86 if(pInput->KeyIsPressed(Key))
87 {
88 Mask |= GetModifierMaskOfKey(Key);
89 }
90 }
91
92 return Mask;
93}
94
95int CBinds::GetModifierMaskOfKey(int Key)
96{
97 switch(Key)
98 {
99 case KEY_LSHIFT:
100 case KEY_RSHIFT:
101 return 1 << KeyModifier::SHIFT;
102 case KEY_LCTRL:
103 case KEY_RCTRL:
104 return 1 << KeyModifier::CTRL;
105 case KEY_LALT:
106 case KEY_RALT:
107 return 1 << KeyModifier::ALT;
108 case KEY_LGUI:
109 case KEY_RGUI:
110 return 1 << KeyModifier::GUI;
111 default:
112 return KeyModifier::NONE;
113 }
114}
115
116bool CBinds::OnInput(const IInput::CEvent &Event)
117{
118 if((Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_RELEASE)) == 0)
119 return false;
120
121 const int KeyModifierMask = GetModifierMaskOfKey(Key: Event.m_Key);
122 const int ModifierMask = GetModifierMask(pInput: Input()) & ~KeyModifierMask;
123
124 bool Handled = false;
125
126 if(Event.m_Flags & IInput::FLAG_PRESS)
127 {
128 auto ActiveBind = std::find_if(first: m_vActiveBinds.begin(), last: m_vActiveBinds.end(), pred: [&](const CBindSlot &Bind) {
129 return Event.m_Key == Bind.m_Key;
130 });
131 if(ActiveBind == m_vActiveBinds.end())
132 {
133 const auto &&OnKeyPress = [&](int Mask) {
134 const char *pBind = m_aapKeyBindings[Mask][Event.m_Key];
135 if(g_Config.m_ClSubTickAiming)
136 {
137 if(str_comp(a: "+fire", b: pBind) == 0 || str_comp(a: "+hook", b: pBind) == 0)
138 {
139 m_MouseOnAction = true;
140 }
141 }
142 Console()->ExecuteLineStroked(Stroke: 1, pStr: pBind, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
143 m_vActiveBinds.emplace_back(args: Event.m_Key, args&: Mask);
144 };
145
146 if(m_aapKeyBindings[ModifierMask][Event.m_Key])
147 {
148 OnKeyPress(ModifierMask);
149 Handled = true;
150 }
151 else if(m_aapKeyBindings[KeyModifier::NONE][Event.m_Key] &&
152 ModifierMask != ((1 << KeyModifier::CTRL) | (1 << KeyModifier::SHIFT)) &&
153 ModifierMask != ((1 << KeyModifier::GUI) | (1 << KeyModifier::SHIFT)))
154 {
155 OnKeyPress(KeyModifier::NONE);
156 Handled = true;
157 }
158 }
159 else
160 {
161 // Repeat active bind while key is held down
162 // Have to check for nullptr again because the previous execute can unbind itself
163 if(m_aapKeyBindings[ActiveBind->m_ModifierMask][ActiveBind->m_Key])
164 {
165 Console()->ExecuteLineStroked(Stroke: 1, pStr: m_aapKeyBindings[ActiveBind->m_ModifierMask][ActiveBind->m_Key], ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
166 }
167 Handled = true;
168 }
169 }
170
171 if(Event.m_Flags & IInput::FLAG_RELEASE)
172 {
173 const auto &&OnKeyRelease = [&](const CBindSlot &Bind) {
174 // Prevent binds from being deactivated while chat, console and menus are open, as these components will
175 // still allow key release events to be forwarded to this component, so the active binds can be cleared.
176 if(GameClient()->m_Chat.IsActive() ||
177 GameClient()->m_GameConsole.IsActive() ||
178 GameClient()->m_Menus.IsActive())
179 {
180 return;
181 }
182 // Have to check for nullptr again because the previous execute can unbind itself
183 if(!m_aapKeyBindings[Bind.m_ModifierMask][Bind.m_Key])
184 {
185 return;
186 }
187 Console()->ExecuteLineStroked(Stroke: 0, pStr: m_aapKeyBindings[Bind.m_ModifierMask][Bind.m_Key], ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
188 };
189
190 // Release active bind that uses this primary key
191 auto ActiveBind = std::find_if(first: m_vActiveBinds.begin(), last: m_vActiveBinds.end(), pred: [&](const CBindSlot &Bind) {
192 return Event.m_Key == Bind.m_Key;
193 });
194 if(ActiveBind != m_vActiveBinds.end())
195 {
196 OnKeyRelease(*ActiveBind);
197 m_vActiveBinds.erase(position: ActiveBind);
198 Handled = true;
199 }
200
201 // Release all active binds that use this modifier key
202 if(KeyModifierMask != KeyModifier::NONE)
203 {
204 while(true)
205 {
206 auto ActiveModifierBind = std::find_if(first: m_vActiveBinds.begin(), last: m_vActiveBinds.end(), pred: [&](const CBindSlot &Bind) {
207 return (Bind.m_ModifierMask & KeyModifierMask) != 0;
208 });
209 if(ActiveModifierBind == m_vActiveBinds.end())
210 break;
211 OnKeyRelease(*ActiveModifierBind);
212 m_vActiveBinds.erase(position: ActiveModifierBind);
213 Handled = true;
214 }
215 }
216 }
217
218 return Handled;
219}
220
221void CBinds::UnbindAll()
222{
223 for(auto &apKeyBinding : m_aapKeyBindings)
224 {
225 for(auto &pKeyBinding : apKeyBinding)
226 {
227 free(ptr: pKeyBinding);
228 pKeyBinding = nullptr;
229 }
230 }
231}
232
233const char *CBinds::Get(int KeyId, int ModifierCombination) const
234{
235 dbg_assert(KeyId >= KEY_FIRST && KeyId < KEY_LAST, "KeyId invalid");
236 dbg_assert(ModifierCombination >= KeyModifier::NONE && ModifierCombination < KeyModifier::COMBINATION_COUNT, "ModifierCombination invalid");
237 return m_aapKeyBindings[ModifierCombination][KeyId] ? m_aapKeyBindings[ModifierCombination][KeyId] : "";
238}
239
240const char *CBinds::Get(const CBindSlot &BindSlot) const
241{
242 return Get(KeyId: BindSlot.m_Key, ModifierCombination: BindSlot.m_ModifierMask);
243}
244
245void CBinds::GetKey(const char *pBindStr, char *pBuf, size_t BufSize) const
246{
247 pBuf[0] = '\0';
248 for(int Modifier = KeyModifier::NONE; Modifier < KeyModifier::COMBINATION_COUNT; Modifier++)
249 {
250 for(int KeyId = KEY_FIRST; KeyId < KEY_LAST; KeyId++)
251 {
252 const char *pBind = Get(KeyId, ModifierCombination: Modifier);
253 if(!pBind[0])
254 continue;
255
256 if(str_comp(a: pBind, b: pBindStr) == 0)
257 {
258 GetKeyBindName(Key: KeyId, ModifierMask: Modifier, pBuf, BufSize);
259 return;
260 }
261 }
262 }
263}
264
265void CBinds::SetDefaults()
266{
267 UnbindAll();
268
269 Bind(KeyId: KEY_F1, pStr: "toggle_local_console");
270 Bind(KeyId: KEY_F2, pStr: "toggle_remote_console");
271 Bind(KeyId: KEY_TAB, pStr: "+scoreboard");
272 Bind(KeyId: KEY_EQUALS, pStr: "+statboard");
273 Bind(KeyId: KEY_F10, pStr: "screenshot");
274
275 Bind(KeyId: KEY_A, pStr: "+left");
276 Bind(KeyId: KEY_D, pStr: "+right");
277
278 Bind(KeyId: KEY_SPACE, pStr: "+jump");
279 Bind(KeyId: KEY_MOUSE_1, pStr: "+fire");
280 Bind(KeyId: KEY_MOUSE_2, pStr: "+hook");
281 Bind(KeyId: KEY_LSHIFT, pStr: "+emote");
282 Bind(KeyId: KEY_RETURN, pStr: "+show_chat; chat all");
283 Bind(KeyId: KEY_RIGHT, pStr: "spectate_next");
284 Bind(KeyId: KEY_LEFT, pStr: "spectate_previous");
285 Bind(KeyId: KEY_RSHIFT, pStr: "+spectate");
286
287 Bind(KeyId: KEY_1, pStr: "+weapon1");
288 Bind(KeyId: KEY_2, pStr: "+weapon2");
289 Bind(KeyId: KEY_3, pStr: "+weapon3");
290 Bind(KeyId: KEY_4, pStr: "+weapon4");
291 Bind(KeyId: KEY_5, pStr: "+weapon5");
292
293 Bind(KeyId: KEY_MOUSE_WHEEL_UP, pStr: "+prevweapon");
294 Bind(KeyId: KEY_MOUSE_WHEEL_DOWN, pStr: "+nextweapon");
295
296 Bind(KeyId: KEY_T, pStr: "+show_chat; chat all");
297 Bind(KeyId: KEY_Y, pStr: "+show_chat; chat team");
298 Bind(KeyId: KEY_U, pStr: "+show_chat");
299 Bind(KeyId: KEY_I, pStr: "+show_chat; chat all /c ");
300
301 Bind(KeyId: KEY_F3, pStr: "vote yes");
302 Bind(KeyId: KEY_F4, pStr: "vote no");
303
304 Bind(KeyId: KEY_K, pStr: "kill");
305 Bind(KeyId: KEY_Q, pStr: "say /spec");
306 Bind(KeyId: KEY_P, pStr: "say /pause");
307
308 g_Config.m_ClDDRaceBindsSet = 0;
309 SetDDRaceBinds(false);
310}
311
312void CBinds::OnConsoleInit()
313{
314 ConfigManager()->RegisterCallback(pfnFunc: ConfigSaveCallback, pUserData: this);
315
316 Console()->Register(pName: "bind", pParams: "s[key] ?r[command]", Flags: CFGFLAG_CLIENT, pfnFunc: ConBind, pUser: this, pHelp: "Bind key to execute a command or view keybindings");
317 Console()->Register(pName: "binds", pParams: "?s[key]", Flags: CFGFLAG_CLIENT, pfnFunc: ConBinds, pUser: this, pHelp: "Print command executed by this keybinding or all binds");
318 Console()->Register(pName: "unbind", pParams: "s[key]", Flags: CFGFLAG_CLIENT, pfnFunc: ConUnbind, pUser: this, pHelp: "Unbind key");
319 Console()->Register(pName: "unbindall", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConUnbindAll, pUser: this, pHelp: "Unbind all keys");
320
321 SetDefaults();
322}
323
324void CBinds::ConBind(IConsole::IResult *pResult, void *pUserData)
325{
326 CBinds *pBinds = (CBinds *)pUserData;
327 const char *pBindStr = pResult->GetString(Index: 0);
328 const CBindSlot BindSlot = pBinds->GetBindSlot(pBindString: pBindStr);
329
330 if(!BindSlot.m_Key)
331 {
332 log_info_color(BIND_PRINT_COLOR, "binds", "key %s not found", pBindStr);
333 return;
334 }
335
336 if(pResult->NumArguments() == 1)
337 {
338 ConBinds(pResult, pUserData);
339 return;
340 }
341
342 pBinds->Bind(KeyId: BindSlot.m_Key, pStr: pResult->GetString(Index: 1), FreeOnly: false, ModifierCombination: BindSlot.m_ModifierMask);
343}
344
345void CBinds::ConBinds(IConsole::IResult *pResult, void *pUserData)
346{
347 CBinds *pBinds = (CBinds *)pUserData;
348 if(pResult->NumArguments() == 1)
349 {
350 const char *pKeyName = pResult->GetString(Index: 0);
351 const CBindSlot BindSlot = pBinds->GetBindSlot(pBindString: pKeyName);
352 if(!BindSlot.m_Key)
353 {
354 log_info_color(BIND_PRINT_COLOR, "binds", "key '%s' not found", pKeyName);
355 }
356 else
357 {
358 if(!pBinds->m_aapKeyBindings[BindSlot.m_ModifierMask][BindSlot.m_Key])
359 log_info_color(BIND_PRINT_COLOR, "binds", "%s is not bound", pKeyName);
360 else
361 {
362 char *pBuf = pBinds->GetKeyBindCommand(ModifierCombination: BindSlot.m_ModifierMask, Key: BindSlot.m_Key);
363 log_info_color(BIND_PRINT_COLOR, "binds", "%s", pBuf);
364 free(ptr: pBuf);
365 }
366 }
367 }
368 else
369 {
370 for(int Modifier = KeyModifier::NONE; Modifier < KeyModifier::COMBINATION_COUNT; Modifier++)
371 {
372 for(int Key = KEY_FIRST; Key < KEY_LAST; Key++)
373 {
374 if(!pBinds->m_aapKeyBindings[Modifier][Key])
375 continue;
376 char *pBuf = pBinds->GetKeyBindCommand(ModifierCombination: Modifier, Key);
377 log_info_color(BIND_PRINT_COLOR, "binds", "%s", pBuf);
378 free(ptr: pBuf);
379 }
380 }
381 }
382}
383
384void CBinds::ConUnbind(IConsole::IResult *pResult, void *pUserData)
385{
386 CBinds *pBinds = (CBinds *)pUserData;
387 const char *pKeyName = pResult->GetString(Index: 0);
388 const CBindSlot BindSlot = pBinds->GetBindSlot(pBindString: pKeyName);
389
390 if(!BindSlot.m_Key)
391 {
392 log_info_color(BIND_PRINT_COLOR, "binds", "key %s not found", pKeyName);
393 return;
394 }
395
396 pBinds->Bind(KeyId: BindSlot.m_Key, pStr: "", FreeOnly: false, ModifierCombination: BindSlot.m_ModifierMask);
397}
398
399void CBinds::ConUnbindAll(IConsole::IResult *pResult, void *pUserData)
400{
401 CBinds *pBinds = (CBinds *)pUserData;
402 pBinds->UnbindAll();
403}
404
405CBindSlot CBinds::GetBindSlot(const char *pBindString) const
406{
407 int ModifierMask = KeyModifier::NONE;
408 char aMod[32];
409 aMod[0] = '\0';
410 const char *pKey = str_next_token(str: pBindString, delim: "+", buffer: aMod, buffer_size: sizeof(aMod));
411 while(aMod[0] && *(pKey))
412 {
413 if(!str_comp_nocase(a: aMod, b: "shift"))
414 ModifierMask |= (1 << KeyModifier::SHIFT);
415 else if(!str_comp_nocase(a: aMod, b: "ctrl"))
416 ModifierMask |= (1 << KeyModifier::CTRL);
417 else if(!str_comp_nocase(a: aMod, b: "alt"))
418 ModifierMask |= (1 << KeyModifier::ALT);
419 else if(!str_comp_nocase(a: aMod, b: "gui"))
420 ModifierMask |= (1 << KeyModifier::GUI);
421 else
422 return EMPTY_BIND_SLOT;
423
424 if(str_find(haystack: pKey + 1, needle: "+"))
425 pKey = str_next_token(str: pKey + 1, delim: "+", buffer: aMod, buffer_size: sizeof(aMod));
426 else
427 break;
428 }
429 return {Input()->FindKeyByName(pKeyName: ModifierMask == KeyModifier::NONE ? aMod : pKey + 1), ModifierMask};
430}
431
432const char *CBinds::GetModifierName(int Modifier)
433{
434 switch(Modifier)
435 {
436 case KeyModifier::SHIFT:
437 return "shift";
438 case KeyModifier::CTRL:
439 return "ctrl";
440 case KeyModifier::ALT:
441 return "alt";
442 case KeyModifier::GUI:
443 return "gui";
444 default:
445 dbg_assert_failed("Modifier invalid: %d", Modifier);
446 }
447}
448
449void CBinds::GetKeyBindName(int Key, int ModifierMask, char *pBuf, size_t BufSize) const
450{
451 pBuf[0] = '\0';
452 for(int Modifier = KeyModifier::CTRL; Modifier < KeyModifier::COUNT; Modifier++)
453 {
454 if(ModifierMask & (1 << Modifier))
455 {
456 str_append(dst: pBuf, src: GetModifierName(Modifier), dst_size: BufSize);
457 str_append(dst: pBuf, src: "+", dst_size: BufSize);
458 }
459 }
460 str_append(dst: pBuf, src: Input()->KeyName(Key), dst_size: BufSize);
461}
462
463char *CBinds::GetKeyBindCommand(int ModifierCombination, int Key) const
464{
465 char aBindName[128];
466 GetKeyBindName(Key, ModifierMask: ModifierCombination, pBuf: aBindName, BufSize: sizeof(aBindName));
467 // worst case the str_escape can double the string length
468 int Size = str_length(str: m_aapKeyBindings[ModifierCombination][Key]) * 2 + str_length(str: aBindName) + 16;
469 auto *pBuf = static_cast<char *>(malloc(size: Size));
470 str_format(buffer: pBuf, buffer_size: Size, format: "bind %s \"", aBindName);
471 char *pDst = pBuf + str_length(str: pBuf);
472 // process the string. we need to escape some characters
473 str_escape(dst: &pDst, src: m_aapKeyBindings[ModifierCombination][Key], end: pBuf + Size);
474 str_append(dst: pBuf, src: "\"", dst_size: Size);
475 return pBuf;
476}
477
478void CBinds::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
479{
480 CBinds *pSelf = (CBinds *)pUserData;
481
482 pConfigManager->WriteLine(pLine: "unbindall");
483 for(int Modifier = KeyModifier::NONE; Modifier < KeyModifier::COMBINATION_COUNT; Modifier++)
484 {
485 for(int Key = KEY_FIRST; Key < KEY_LAST; Key++)
486 {
487 if(!pSelf->m_aapKeyBindings[Modifier][Key])
488 continue;
489 char *pBuf = pSelf->GetKeyBindCommand(ModifierCombination: Modifier, Key);
490 pConfigManager->WriteLine(pLine: pBuf);
491 free(ptr: pBuf);
492 }
493 }
494}
495
496// DDRace
497
498void CBinds::SetDDRaceBinds(bool FreeOnly)
499{
500 if(g_Config.m_ClDDRaceBindsSet < 1)
501 {
502 Bind(KeyId: KEY_KP_PLUS, pStr: "zoom+", FreeOnly);
503 Bind(KeyId: KEY_KP_MINUS, pStr: "zoom-", FreeOnly);
504 Bind(KeyId: KEY_KP_MULTIPLY, pStr: "zoom", FreeOnly);
505 Bind(KeyId: KEY_PAUSE, pStr: "say /pause", FreeOnly);
506 Bind(KeyId: KEY_UP, pStr: "+jump", FreeOnly);
507 Bind(KeyId: KEY_LEFT, pStr: "+left", FreeOnly);
508 Bind(KeyId: KEY_RIGHT, pStr: "+right", FreeOnly);
509 Bind(KeyId: KEY_LEFTBRACKET, pStr: "+prevweapon", FreeOnly);
510 Bind(KeyId: KEY_RIGHTBRACKET, pStr: "+nextweapon", FreeOnly);
511 Bind(KeyId: KEY_C, pStr: "say /rank", FreeOnly);
512 Bind(KeyId: KEY_V, pStr: "say /info", FreeOnly);
513 Bind(KeyId: KEY_B, pStr: "say /top5", FreeOnly);
514 Bind(KeyId: KEY_S, pStr: "+showhookcoll", FreeOnly);
515 Bind(KeyId: KEY_X, pStr: "toggle cl_dummy 0 1", FreeOnly);
516 Bind(KeyId: KEY_H, pStr: "toggle cl_dummy_hammer 0 1", FreeOnly);
517 Bind(KeyId: KEY_SLASH, pStr: "+show_chat; chat all /", FreeOnly);
518 Bind(KeyId: KEY_KP_0, pStr: "say /emote normal 999999", FreeOnly);
519 Bind(KeyId: KEY_KP_1, pStr: "say /emote happy 999999", FreeOnly);
520 Bind(KeyId: KEY_KP_2, pStr: "say /emote angry 999999", FreeOnly);
521 Bind(KeyId: KEY_KP_3, pStr: "say /emote pain 999999", FreeOnly);
522 Bind(KeyId: KEY_KP_4, pStr: "say /emote surprise 999999", FreeOnly);
523 Bind(KeyId: KEY_KP_5, pStr: "say /emote blink 999999", FreeOnly);
524 Bind(KeyId: KEY_MINUS, pStr: "spectate_previous", FreeOnly);
525 Bind(KeyId: KEY_EQUALS, pStr: "spectate_next", FreeOnly);
526 }
527
528 if(g_Config.m_ClDDRaceBindsSet < 2)
529 {
530 const bool DontModifySpectate = FreeOnly && str_comp(a: Get(KeyId: KEY_MOUSE_3, ModifierCombination: KeyModifier::NONE), b: "+spectate") != 0;
531 Bind(KeyId: KEY_MOUSE_3, pStr: "toggle_scoreboard_cursor; +spectate", FreeOnly: DontModifySpectate);
532 Bind(KeyId: KEY_LALT, pStr: "toggle_scoreboard_cursor", FreeOnly);
533 }
534
535 g_Config.m_ClDDRaceBindsSet = 2;
536}
537