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