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