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