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 "controls.h"
4
5#include <base/math.h>
6#include <base/time.h>
7#include <base/vmath.h>
8
9#include <engine/client.h>
10#include <engine/shared/config.h>
11
12#include <generated/protocol.h>
13
14#include <game/client/components/camera.h>
15#include <game/client/components/chat.h>
16#include <game/client/components/menus.h>
17#include <game/client/components/scoreboard.h>
18#include <game/client/gameclient.h>
19#include <game/collision.h>
20
21CControls::CControls()
22{
23 mem_zero(block: &m_aLastData, size: sizeof(m_aLastData));
24 std::fill(first: std::begin(arr&: m_aMousePos), last: std::end(arr&: m_aMousePos), value: vec2(0.0f, 0.0f));
25 std::fill(first: std::begin(arr&: m_aMousePosOnAction), last: std::end(arr&: m_aMousePosOnAction), value: vec2(0.0f, 0.0f));
26 std::fill(first: std::begin(arr&: m_aTargetPos), last: std::end(arr&: m_aTargetPos), value: vec2(0.0f, 0.0f));
27 std::fill(first: std::begin(arr&: m_aMouseInputType), last: std::end(arr&: m_aMouseInputType), value: EMouseInputType::ABSOLUTE);
28}
29
30void CControls::OnReset()
31{
32 ResetInput(Dummy: 0);
33 ResetInput(Dummy: 1);
34
35 for(int &AmmoCount : m_aAmmoCount)
36 AmmoCount = 0;
37
38 m_LastSendTime = 0;
39}
40
41void CControls::ResetInput(int Dummy)
42{
43 m_aLastData[Dummy].m_Direction = 0;
44 // simulate releasing the fire button
45 if((m_aLastData[Dummy].m_Fire & 1) != 0)
46 m_aLastData[Dummy].m_Fire++;
47 m_aLastData[Dummy].m_Fire &= INPUT_STATE_MASK;
48 m_aLastData[Dummy].m_Jump = 0;
49 m_aInputData[Dummy] = m_aLastData[Dummy];
50
51 m_aInputDirectionLeft[Dummy] = 0;
52 m_aInputDirectionRight[Dummy] = 0;
53}
54
55void CControls::OnPlayerDeath()
56{
57 for(int &AmmoCount : m_aAmmoCount)
58 AmmoCount = 0;
59}
60
61struct CInputState
62{
63 CControls *m_pControls;
64 int *m_apVariables[NUM_DUMMIES];
65};
66
67void CControls::ConKeyInputState(IConsole::IResult *pResult, void *pUserData)
68{
69 CInputState *pState = (CInputState *)pUserData;
70
71 if(pState->m_pControls->GameClient()->m_GameInfo.m_BugDDRaceInput && pState->m_pControls->GameClient()->m_Snap.m_SpecInfo.m_Active)
72 return;
73
74 *pState->m_apVariables[g_Config.m_ClDummy] = pResult->GetInteger(Index: 0);
75}
76
77void CControls::ConKeyInputCounter(IConsole::IResult *pResult, void *pUserData)
78{
79 CInputState *pState = (CInputState *)pUserData;
80
81 if((pState->m_pControls->GameClient()->m_GameInfo.m_BugDDRaceInput && pState->m_pControls->GameClient()->m_Snap.m_SpecInfo.m_Active) || pState->m_pControls->GameClient()->m_Spectator.IsActive())
82 return;
83
84 int *pVariable = pState->m_apVariables[g_Config.m_ClDummy];
85 if(((*pVariable) & 1) != pResult->GetInteger(Index: 0))
86 (*pVariable)++;
87 *pVariable &= INPUT_STATE_MASK;
88}
89
90struct CInputSet
91{
92 CControls *m_pControls;
93 int *m_apVariables[NUM_DUMMIES];
94 int m_Value;
95};
96
97void CControls::ConKeyInputSet(IConsole::IResult *pResult, void *pUserData)
98{
99 CInputSet *pSet = (CInputSet *)pUserData;
100 if(pResult->GetInteger(Index: 0))
101 {
102 *pSet->m_apVariables[g_Config.m_ClDummy] = pSet->m_Value;
103 }
104}
105
106void CControls::ConKeyInputNextPrevWeapon(IConsole::IResult *pResult, void *pUserData)
107{
108 CInputSet *pSet = (CInputSet *)pUserData;
109 ConKeyInputCounter(pResult, pUserData: pSet);
110 pSet->m_pControls->m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = 0;
111}
112
113void CControls::OnConsoleInit()
114{
115 // game commands
116 {
117 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aInputDirectionLeft[0], &m_aInputDirectionLeft[1]}};
118 Console()->Register(pName: "+left", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputState, pUser: &s_State, pHelp: "Move left");
119 }
120 {
121 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aInputDirectionRight[0], &m_aInputDirectionRight[1]}};
122 Console()->Register(pName: "+right", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputState, pUser: &s_State, pHelp: "Move right");
123 }
124 {
125 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_Jump, &m_aInputData[1].m_Jump}};
126 Console()->Register(pName: "+jump", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputState, pUser: &s_State, pHelp: "Jump");
127 }
128 {
129 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_Hook, &m_aInputData[1].m_Hook}};
130 Console()->Register(pName: "+hook", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputState, pUser: &s_State, pHelp: "Hook");
131 }
132 {
133 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_Fire, &m_aInputData[1].m_Fire}};
134 Console()->Register(pName: "+fire", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputCounter, pUser: &s_State, pHelp: "Fire");
135 }
136 {
137 static CInputState s_State = {.m_pControls: this, .m_apVariables: {&m_aShowHookColl[0], &m_aShowHookColl[1]}};
138 Console()->Register(pName: "+showhookcoll", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputState, pUser: &s_State, pHelp: "Show Hook Collision");
139 }
140
141 {
142 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, .m_Value: 1};
143 Console()->Register(pName: "+weapon1", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputSet, pUser: &s_Set, pHelp: "Switch to hammer");
144 }
145 {
146 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, .m_Value: 2};
147 Console()->Register(pName: "+weapon2", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputSet, pUser: &s_Set, pHelp: "Switch to gun");
148 }
149 {
150 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, .m_Value: 3};
151 Console()->Register(pName: "+weapon3", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputSet, pUser: &s_Set, pHelp: "Switch to shotgun");
152 }
153 {
154 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, .m_Value: 4};
155 Console()->Register(pName: "+weapon4", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputSet, pUser: &s_Set, pHelp: "Switch to grenade");
156 }
157 {
158 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, .m_Value: 5};
159 Console()->Register(pName: "+weapon5", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputSet, pUser: &s_Set, pHelp: "Switch to laser");
160 }
161
162 {
163 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_NextWeapon, &m_aInputData[1].m_NextWeapon}, .m_Value: 0};
164 Console()->Register(pName: "+nextweapon", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputNextPrevWeapon, pUser: &s_Set, pHelp: "Switch to next weapon");
165 }
166 {
167 static CInputSet s_Set = {.m_pControls: this, .m_apVariables: {&m_aInputData[0].m_PrevWeapon, &m_aInputData[1].m_PrevWeapon}, .m_Value: 0};
168 Console()->Register(pName: "+prevweapon", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyInputNextPrevWeapon, pUser: &s_Set, pHelp: "Switch to previous weapon");
169 }
170}
171
172void CControls::OnMessage(int Msg, void *pRawMsg)
173{
174 if(Msg == NETMSGTYPE_SV_WEAPONPICKUP)
175 {
176 CNetMsg_Sv_WeaponPickup *pMsg = (CNetMsg_Sv_WeaponPickup *)pRawMsg;
177 if(g_Config.m_ClAutoswitchWeapons)
178 m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = pMsg->m_Weapon + 1;
179 // We don't really know ammo count, until we'll switch to that weapon, but any non-zero count will suffice here
180 m_aAmmoCount[maximum(a: 0, b: pMsg->m_Weapon % NUM_WEAPONS)] = 10;
181 }
182}
183
184int CControls::SnapInput(int *pData)
185{
186 // update player state
187 if(GameClient()->m_Chat.IsActive())
188 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags = PLAYERFLAG_CHATTING;
189 else if(GameClient()->m_Menus.IsActive())
190 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags = PLAYERFLAG_IN_MENU;
191 else
192 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags = PLAYERFLAG_PLAYING;
193
194 if(GameClient()->m_Scoreboard.IsActive())
195 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SCOREBOARD;
196
197 if(Client()->ServerCapAnyPlayerFlag() && GameClient()->m_Controls.m_aShowHookColl[g_Config.m_ClDummy])
198 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_AIM;
199
200 if(Client()->ServerCapAnyPlayerFlag() && GameClient()->m_Camera.CamType() == CCamera::CAMTYPE_SPEC)
201 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SPEC_CAM;
202
203 switch(m_aMouseInputType[g_Config.m_ClDummy])
204 {
205 case CControls::EMouseInputType::AUTOMATED:
206 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_INPUT_ABSOLUTE;
207 break;
208 case CControls::EMouseInputType::ABSOLUTE:
209 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_INPUT_ABSOLUTE | PLAYERFLAG_INPUT_MANUAL;
210 break;
211 case CControls::EMouseInputType::RELATIVE:
212 m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_INPUT_MANUAL;
213 break;
214 }
215
216 bool Send = m_aLastData[g_Config.m_ClDummy].m_PlayerFlags != m_aInputData[g_Config.m_ClDummy].m_PlayerFlags;
217
218 m_aLastData[g_Config.m_ClDummy].m_PlayerFlags = m_aInputData[g_Config.m_ClDummy].m_PlayerFlags;
219
220 // we freeze the input if chat or menu is activated
221 if(!(m_aInputData[g_Config.m_ClDummy].m_PlayerFlags & PLAYERFLAG_PLAYING))
222 {
223 if(!GameClient()->m_GameInfo.m_BugDDRaceInput)
224 ResetInput(Dummy: g_Config.m_ClDummy);
225
226 mem_copy(dest: pData, source: &m_aInputData[g_Config.m_ClDummy], size: sizeof(m_aInputData[0]));
227
228 // set the target anyway though so that we can keep seeing our surroundings,
229 // even if chat or menu are activated
230 m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePos[g_Config.m_ClDummy].x;
231 m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePos[g_Config.m_ClDummy].y;
232
233 // send once a second just to be sure
234 Send = Send || time_get() > m_LastSendTime + time_freq();
235 }
236 else
237 {
238 m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePos[g_Config.m_ClDummy].x;
239 m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePos[g_Config.m_ClDummy].y;
240
241 if(g_Config.m_ClSubTickAiming && m_aMousePosOnAction[g_Config.m_ClDummy] != vec2(0.0f, 0.0f))
242 {
243 m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePosOnAction[g_Config.m_ClDummy].x;
244 m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePosOnAction[g_Config.m_ClDummy].y;
245 m_aMousePosOnAction[g_Config.m_ClDummy] = vec2(0.0f, 0.0f);
246 }
247
248 if(!m_aInputData[g_Config.m_ClDummy].m_TargetX && !m_aInputData[g_Config.m_ClDummy].m_TargetY)
249 {
250 m_aInputData[g_Config.m_ClDummy].m_TargetX = 1;
251 m_aMousePos[g_Config.m_ClDummy].x = 1;
252 }
253
254 // set direction
255 m_aInputData[g_Config.m_ClDummy].m_Direction = 0;
256 if(m_aInputDirectionLeft[g_Config.m_ClDummy] && !m_aInputDirectionRight[g_Config.m_ClDummy])
257 m_aInputData[g_Config.m_ClDummy].m_Direction = -1;
258 if(!m_aInputDirectionLeft[g_Config.m_ClDummy] && m_aInputDirectionRight[g_Config.m_ClDummy])
259 m_aInputData[g_Config.m_ClDummy].m_Direction = 1;
260
261 // dummy copy moves
262 if(g_Config.m_ClDummyCopyMoves)
263 {
264 CNetObj_PlayerInput *pDummyInput = &GameClient()->m_DummyInput;
265
266 // Don't copy any input to dummy when spectating others
267 if(!GameClient()->m_Snap.m_SpecInfo.m_Active || GameClient()->m_Snap.m_SpecInfo.m_SpectatorId < 0)
268 {
269 pDummyInput->m_Direction = m_aInputData[g_Config.m_ClDummy].m_Direction;
270 pDummyInput->m_Hook = m_aInputData[g_Config.m_ClDummy].m_Hook;
271 pDummyInput->m_Jump = m_aInputData[g_Config.m_ClDummy].m_Jump;
272 pDummyInput->m_PlayerFlags = m_aInputData[g_Config.m_ClDummy].m_PlayerFlags;
273 pDummyInput->m_TargetX = m_aInputData[g_Config.m_ClDummy].m_TargetX;
274 pDummyInput->m_TargetY = m_aInputData[g_Config.m_ClDummy].m_TargetY;
275 pDummyInput->m_WantedWeapon = m_aInputData[g_Config.m_ClDummy].m_WantedWeapon;
276
277 if(!g_Config.m_ClDummyControl)
278 pDummyInput->m_Fire += m_aInputData[g_Config.m_ClDummy].m_Fire - m_aLastData[g_Config.m_ClDummy].m_Fire;
279
280 pDummyInput->m_NextWeapon += m_aInputData[g_Config.m_ClDummy].m_NextWeapon - m_aLastData[g_Config.m_ClDummy].m_NextWeapon;
281 pDummyInput->m_PrevWeapon += m_aInputData[g_Config.m_ClDummy].m_PrevWeapon - m_aLastData[g_Config.m_ClDummy].m_PrevWeapon;
282 }
283
284 m_aInputData[!g_Config.m_ClDummy] = *pDummyInput;
285 }
286
287 if(g_Config.m_ClDummyControl)
288 {
289 CNetObj_PlayerInput *pDummyInput = &GameClient()->m_DummyInput;
290 pDummyInput->m_Jump = g_Config.m_ClDummyJump;
291
292 if(g_Config.m_ClDummyFire)
293 pDummyInput->m_Fire = g_Config.m_ClDummyFire;
294 else if((pDummyInput->m_Fire & 1) != 0)
295 pDummyInput->m_Fire++;
296
297 pDummyInput->m_Hook = g_Config.m_ClDummyHook;
298 m_aInputData[!g_Config.m_ClDummy] = *pDummyInput;
299 }
300
301 // stress testing
302 if(g_Config.m_DbgStress)
303 {
304 float t = Client()->LocalTime();
305 mem_zero(block: &m_aInputData[g_Config.m_ClDummy], size: sizeof(m_aInputData[0]));
306
307 m_aInputData[g_Config.m_ClDummy].m_Direction = ((int)t / 2) & 1;
308 m_aInputData[g_Config.m_ClDummy].m_Jump = ((int)t);
309 m_aInputData[g_Config.m_ClDummy].m_Fire = ((int)(t * 10));
310 m_aInputData[g_Config.m_ClDummy].m_Hook = ((int)(t * 2)) & 1;
311 m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = ((int)t) % NUM_WEAPONS;
312 m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)(std::sin(x: t * 3) * 100.0f);
313 m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)(std::cos(x: t * 3) * 100.0f);
314 }
315
316 // check if we need to send input
317 Send = Send || m_aInputData[g_Config.m_ClDummy].m_Direction != m_aLastData[g_Config.m_ClDummy].m_Direction;
318 Send = Send || m_aInputData[g_Config.m_ClDummy].m_Jump != m_aLastData[g_Config.m_ClDummy].m_Jump;
319 Send = Send || m_aInputData[g_Config.m_ClDummy].m_Fire != m_aLastData[g_Config.m_ClDummy].m_Fire;
320 Send = Send || m_aInputData[g_Config.m_ClDummy].m_Hook != m_aLastData[g_Config.m_ClDummy].m_Hook;
321 Send = Send || m_aInputData[g_Config.m_ClDummy].m_WantedWeapon != m_aLastData[g_Config.m_ClDummy].m_WantedWeapon;
322 Send = Send || m_aInputData[g_Config.m_ClDummy].m_NextWeapon != m_aLastData[g_Config.m_ClDummy].m_NextWeapon;
323 Send = Send || m_aInputData[g_Config.m_ClDummy].m_PrevWeapon != m_aLastData[g_Config.m_ClDummy].m_PrevWeapon;
324 Send = Send || time_get() > m_LastSendTime + time_freq() / 25; // send at least 25 Hz
325 Send = Send || (GameClient()->m_Snap.m_pLocalCharacter && GameClient()->m_Snap.m_pLocalCharacter->m_Weapon == WEAPON_NINJA && (m_aInputData[g_Config.m_ClDummy].m_Direction || m_aInputData[g_Config.m_ClDummy].m_Jump || m_aInputData[g_Config.m_ClDummy].m_Hook));
326 }
327
328 // copy and return size
329 m_aLastData[g_Config.m_ClDummy] = m_aInputData[g_Config.m_ClDummy];
330
331 if(!Send)
332 return 0;
333
334 m_LastSendTime = time_get();
335 mem_copy(dest: pData, source: &m_aInputData[g_Config.m_ClDummy], size: sizeof(m_aInputData[0]));
336 return sizeof(m_aInputData[0]);
337}
338
339void CControls::OnRender()
340{
341 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
342 return;
343
344 if(g_Config.m_ClAutoswitchWeaponsOutOfAmmo && !GameClient()->m_GameInfo.m_UnlimitedAmmo && GameClient()->m_Snap.m_pLocalCharacter)
345 {
346 // Keep track of ammo count, we know weapon ammo only when we switch to that weapon, this is tracked on server and protocol does not track that
347 m_aAmmoCount[maximum(a: 0, b: GameClient()->m_Snap.m_pLocalCharacter->m_Weapon % NUM_WEAPONS)] = GameClient()->m_Snap.m_pLocalCharacter->m_AmmoCount;
348 // Autoswitch weapon if we're out of ammo
349 if(m_aInputData[g_Config.m_ClDummy].m_Fire % 2 != 0 &&
350 GameClient()->m_Snap.m_pLocalCharacter->m_AmmoCount == 0 &&
351 GameClient()->m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_HAMMER &&
352 GameClient()->m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA)
353 {
354 int Weapon;
355 for(Weapon = WEAPON_LASER; Weapon > WEAPON_GUN; Weapon--)
356 {
357 if(Weapon == GameClient()->m_Snap.m_pLocalCharacter->m_Weapon)
358 continue;
359 if(m_aAmmoCount[Weapon] > 0)
360 break;
361 }
362 if(Weapon != GameClient()->m_Snap.m_pLocalCharacter->m_Weapon)
363 m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = Weapon + 1;
364 }
365 }
366
367 // update target pos
368 if(GameClient()->m_Snap.m_pGameInfoObj && !GameClient()->m_Snap.m_SpecInfo.m_Active)
369 {
370 // make sure to compensate for smooth dyncam to ensure the cursor stays still in world space if zoomed
371 vec2 DyncamOffsetDelta = GameClient()->m_Camera.m_DyncamTargetCameraOffset - GameClient()->m_Camera.m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy];
372 float Zoom = GameClient()->m_Camera.m_Zoom;
373 m_aTargetPos[g_Config.m_ClDummy] = GameClient()->m_LocalCharacterPos + m_aMousePos[g_Config.m_ClDummy] - DyncamOffsetDelta + DyncamOffsetDelta / Zoom;
374 }
375 else if(GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_Snap.m_SpecInfo.m_UsePosition)
376 {
377 m_aTargetPos[g_Config.m_ClDummy] = GameClient()->m_Snap.m_SpecInfo.m_Position + m_aMousePos[g_Config.m_ClDummy];
378 }
379 else
380 {
381 m_aTargetPos[g_Config.m_ClDummy] = m_aMousePos[g_Config.m_ClDummy];
382 }
383}
384
385bool CControls::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
386{
387 if(GameClient()->m_Snap.m_pGameInfoObj && (GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
388 return false;
389
390 if(CursorType == IInput::CURSOR_JOYSTICK && g_Config.m_InpControllerAbsolute && GameClient()->m_Snap.m_pGameInfoObj && !GameClient()->m_Snap.m_SpecInfo.m_Active)
391 {
392 vec2 AbsoluteDirection;
393 if(Input()->GetActiveJoystick()->Absolute(pX: &AbsoluteDirection.x, pY: &AbsoluteDirection.y))
394 {
395 m_aMousePos[g_Config.m_ClDummy] = AbsoluteDirection * GetMaxMouseDistance();
396 GameClient()->m_Controls.m_aMouseInputType[g_Config.m_ClDummy] = CControls::EMouseInputType::ABSOLUTE;
397 }
398 return true;
399 }
400
401 float Factor = 1.0f;
402 if(g_Config.m_ClDyncam && g_Config.m_ClDyncamMousesens)
403 {
404 Factor = g_Config.m_ClDyncamMousesens / 100.0f;
405 }
406 else
407 {
408 switch(CursorType)
409 {
410 case IInput::CURSOR_MOUSE:
411 Factor = g_Config.m_InpMousesens / 100.0f;
412 break;
413 case IInput::CURSOR_JOYSTICK:
414 Factor = g_Config.m_InpControllerSens / 100.0f;
415 break;
416 default:
417 dbg_assert_failed("CControls::OnCursorMove CursorType %d", (int)CursorType);
418 }
419 }
420
421 if(GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId < 0)
422 Factor *= GameClient()->m_Camera.m_Zoom;
423
424 m_aMousePos[g_Config.m_ClDummy] += vec2(x, y) * Factor;
425 GameClient()->m_Controls.m_aMouseInputType[g_Config.m_ClDummy] = CControls::EMouseInputType::RELATIVE;
426 ClampMousePos();
427 return true;
428}
429
430void CControls::ClampMousePos()
431{
432 if(GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_Snap.m_SpecInfo.m_SpectatorId < 0)
433 {
434 m_aMousePos[g_Config.m_ClDummy].x = std::clamp(val: m_aMousePos[g_Config.m_ClDummy].x, lo: -201.0f * 32, hi: (Collision()->GetWidth() + 201.0f) * 32.0f);
435 m_aMousePos[g_Config.m_ClDummy].y = std::clamp(val: m_aMousePos[g_Config.m_ClDummy].y, lo: -201.0f * 32, hi: (Collision()->GetHeight() + 201.0f) * 32.0f);
436 }
437 else
438 {
439 const float MouseMin = GetMinMouseDistance();
440 const float MouseMax = GetMaxMouseDistance();
441
442 float MouseDistance = length(a: m_aMousePos[g_Config.m_ClDummy]);
443 if(MouseDistance < 0.001f)
444 {
445 m_aMousePos[g_Config.m_ClDummy].x = 0.001f;
446 m_aMousePos[g_Config.m_ClDummy].y = 0;
447 MouseDistance = 0.001f;
448 }
449 if(MouseDistance < MouseMin)
450 m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(v: m_aMousePos[g_Config.m_ClDummy], len: MouseDistance) * MouseMin;
451 MouseDistance = length(a: m_aMousePos[g_Config.m_ClDummy]);
452 if(MouseDistance > MouseMax)
453 m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(v: m_aMousePos[g_Config.m_ClDummy], len: MouseDistance) * MouseMax;
454 }
455}
456
457float CControls::GetMinMouseDistance() const
458{
459 return g_Config.m_ClDyncam ? g_Config.m_ClDyncamMinDistance : g_Config.m_ClMouseMinDistance;
460}
461
462float CControls::GetMaxMouseDistance() const
463{
464 float CameraMaxDistance = 200.0f;
465 float FollowFactor = (g_Config.m_ClDyncam ? g_Config.m_ClDyncamFollowFactor : g_Config.m_ClMouseFollowfactor) / 100.0f;
466 float DeadZone = g_Config.m_ClDyncam ? g_Config.m_ClDyncamDeadzone : g_Config.m_ClMouseDeadzone;
467 float MaxDistance = g_Config.m_ClDyncam ? g_Config.m_ClDyncamMaxDistance : g_Config.m_ClMouseMaxDistance;
468 return minimum(a: (FollowFactor != 0 ? CameraMaxDistance / FollowFactor + DeadZone : MaxDistance), b: MaxDistance);
469}
470