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