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