1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Gamepad control */
18
19#include <C4Include.h>
20#include <C4GamePadCon.h>
21
22#include <C4ObjectCom.h>
23#include <C4Log.h>
24#include <C4Game.h>
25
26#ifdef _WIN32
27#include "C4Windows.h"
28#include <numbers>
29#include <windowsx.h>
30
31static uint32_t POV2Position(DWORD dwPOV, bool fVertical)
32{
33 // POV value is a 360° angle multiplied by 100
34 double dAxis;
35 // Centered
36 if (dwPOV == JOY_POVCENTERED)
37 dAxis = 0.0;
38 // Angle: convert to linear value -100 to +100
39 else
40 {
41 const auto angleRad = dwPOV * std::numbers::pi / (180.0 * 100);
42 dAxis = (fVertical ? -cos(angleRad) : sin(angleRad)) * 100.0;
43 }
44 // Gamepad configuration wants unsigned and gets 0 to 200
45 return static_cast<uint32_t>(dAxis + 100.0);
46}
47
48C4GamePad::C4GamePad(int id) : iRefCount{1}, Buttons{0}, id{id}
49{
50 std::fill(AxisPosis.begin(), AxisPosis.end(), Mid);
51 SetCalibration(&(Config.Gamepads[id].AxisMin[0]), &(Config.Gamepads[id].AxisMax[0]), &(Config.Gamepads[id].AxisCalibrated[0]));
52}
53
54C4GamePad::~C4GamePad()
55{
56 GetCalibration(&(Config.Gamepads[id].AxisMin[0]), &(Config.Gamepads[id].AxisMax[0]), &(Config.Gamepads[id].AxisCalibrated[0]));
57}
58
59void C4GamePad::SetCalibration(uint32_t *pdwAxisMin, uint32_t *pdwAxisMax, bool *pfAxisCalibrated)
60{
61 // params to calibration
62 for (int i = 0; i < MaxCalAxis; ++i)
63 {
64 dwAxisMin[i] = pdwAxisMin[i];
65 dwAxisMax[i] = pdwAxisMax[i];
66 fAxisCalibrated[i] = pfAxisCalibrated[i];
67 }
68}
69
70void C4GamePad::GetCalibration(uint32_t *pdwAxisMin, uint32_t *pdwAxisMax, bool *pfAxisCalibrated)
71{
72 // calibration to params
73 for (int i = 0; i < MaxCalAxis; ++i)
74 {
75 pdwAxisMin[i] = dwAxisMin[i];
76 pdwAxisMax[i] = dwAxisMax[i];
77 pfAxisCalibrated[i] = fAxisCalibrated[i];
78 }
79}
80
81bool C4GamePad::Update()
82{
83 joynfo.dwSize = sizeof(joynfo);
84 joynfo.dwFlags = JOY_RETURNBUTTONS | JOY_RETURNRAWDATA | JOY_RETURNX | JOY_RETURNY | JOY_RETURNZ | JOY_RETURNR | JOY_RETURNU | JOY_RETURNV | JOY_RETURNPOV;
85 return joyGetPosEx(JOYSTICKID1 + id, &joynfo) == JOYERR_NOERROR;
86}
87
88uint32_t C4GamePad::GetCurrentButtons()
89{
90 return joynfo.dwButtons;
91}
92
93constexpr DWORD GetAxisValue(const JOYINFOEX &info, const int axis) noexcept
94{
95 assert(Inside(axis, 0, C4GamePad::MaxCalAxis - 1));
96 switch (axis)
97 {
98 case 0: return info.dwXpos;
99 case 1: return info.dwYpos;
100 case 2: return info.dwZpos;
101 case 3: return info.dwRpos;
102 case 4: return info.dwUpos;
103 case 5: return info.dwVpos;
104 }
105 return -1;
106}
107
108C4GamePad::AxisPos C4GamePad::GetAxisPos(int idAxis)
109{
110 if (idAxis < 0 || idAxis >= MaxAxis) return Mid; // wrong axis
111 // get raw axis data
112 if (idAxis < MaxCalAxis)
113 {
114 uint32_t dwPos = GetAxisValue(joynfo, idAxis);
115 // evaluate axis calibration
116 if (fAxisCalibrated[idAxis])
117 {
118 // update it
119 dwAxisMin[idAxis] = std::min<uint32_t>(dwAxisMin[idAxis], dwPos);
120 dwAxisMax[idAxis] = std::max<uint32_t>(dwAxisMax[idAxis], dwPos);
121 // Calculate center
122 uint32_t dwCenter = (dwAxisMin[idAxis] + dwAxisMax[idAxis]) / 2;
123 // Trigger range is 30% off center
124 uint32_t dwRange = (dwAxisMax[idAxis] - dwCenter) / 3;
125 if (dwPos < dwCenter - dwRange) return Low;
126 if (dwPos > dwCenter + dwRange) return High;
127 }
128 else
129 {
130 // init it
131 dwAxisMin[idAxis] = dwAxisMax[idAxis] = dwPos;
132 fAxisCalibrated[idAxis] = true;
133 }
134 }
135 else
136 {
137 // It's a POV head
138 uint32_t dwPos = POV2Position(joynfo.dwPOV, idAxis == AxisPOV::Y);
139 if (dwPos > 130) return High; else if (dwPos < 70) return Low;
140 }
141 return Mid;
142}
143
144void C4GamePad::IncRef()
145{
146 ++iRefCount;
147}
148
149bool C4GamePad::DecRef()
150{
151 if (!--iRefCount)
152 {
153 delete this;
154 return false;
155 }
156
157 return true;
158}
159
160C4GamePadControl *C4GamePadControl::pInstance = nullptr;
161
162C4GamePadControl::C4GamePadControl()
163{
164 iNumGamepads = 0;
165 // singleton
166 if (!pInstance) pInstance = this;
167}
168
169C4GamePadControl::~C4GamePadControl()
170{
171 if (pInstance == this) pInstance = nullptr;
172 Clear();
173}
174
175void C4GamePadControl::Clear()
176{
177 for (int i = 0; i < C4GamePad::MaxGamePad; ++i)
178 while (Gamepads[i]) Gamepads[i]->DecRef();
179
180 iNumGamepads = 0;
181}
182
183void C4GamePadControl::OpenGamepad(int id)
184{
185 if (!Inside(id, 0, C4GamePad::MaxGamePad - 1)) return;
186
187 if (!(Gamepads[id]))
188 {
189 Gamepads[id] = new C4GamePad{id};
190 ++iNumGamepads;
191 }
192 else
193 {
194 Gamepads[id]->IncRef();
195 }
196}
197
198void C4GamePadControl::CloseGamepad(int id)
199{
200 if (!Inside(id, 0, C4GamePad::MaxGamePad - 1)) return;
201
202 if (!Gamepads[id]->DecRef())
203 {
204 Gamepads[id] = nullptr;
205 --iNumGamepads;
206 }
207}
208
209int C4GamePadControl::GetGamePadCount()
210{
211 JOYINFOEX joy{};
212 joy.dwSize = sizeof(JOYINFOEX); joy.dwFlags = JOY_RETURNALL;
213 int iCnt = 0;
214 while (iCnt < C4GamePad::MaxGamePad && ::joyGetPosEx(iCnt, &joy) == JOYERR_NOERROR) ++iCnt;
215 return iCnt;
216}
217
218const int MaxGamePadButton = 22;
219
220void C4GamePadControl::Execute()
221{
222 // Get gamepad inputs
223 for (auto &pad : Gamepads)
224 {
225 if (!pad || !pad->Update()) continue;
226 for (int iAxis = 0; iAxis < C4GamePad::MaxAxis; ++iAxis)
227 {
228 C4GamePad::AxisPos eAxisPos = pad->GetAxisPos(iAxis), ePrevAxisPos = pad->AxisPosis[iAxis];
229 // Evaluate changes and pass single controls
230 // this is a generic Gamepad-control: Create events
231 if (eAxisPos != ePrevAxisPos)
232 {
233 pad->AxisPosis[iAxis] = eAxisPos;
234 if (ePrevAxisPos != C4GamePad::Mid)
235 Game.DoKeyboardInput(KEY_Gamepad(pad->GetID(), KEY_JOY_Axis(iAxis, (ePrevAxisPos == C4GamePad::High))), KEYEV_Up, false, false, false, false);
236 if (eAxisPos != C4GamePad::Mid)
237 Game.DoKeyboardInput(KEY_Gamepad(pad->GetID(), KEY_JOY_Axis(iAxis, (eAxisPos == C4GamePad::High))), KEYEV_Down, false, false, false, false);
238 }
239 }
240
241 const uint32_t Buttons = pad->GetCurrentButtons();
242 const uint32_t PrevButtons = pad->Buttons;
243 if (Buttons != PrevButtons)
244 {
245 pad->Buttons = Buttons;
246 for (int iButton = 0; iButton < MaxGamePadButton; ++iButton)
247 if ((Buttons & (1 << iButton)) != (PrevButtons & (1 << iButton)))
248 {
249 bool fRelease = ((Buttons & (1 << iButton)) == 0);
250 Game.DoKeyboardInput(KEY_Gamepad(pad->GetID(), KEY_JOY_Button(iButton)), fRelease ? KEYEV_Up : KEYEV_Down, false, false, false, false);
251 }
252 }
253 }
254}
255
256C4GamePadOpener::C4GamePadOpener(int iGamepad)
257{
258 assert(C4GamePadControl::pInstance);
259 this->iGamePad = iGamepad;
260 C4GamePadControl::pInstance->OpenGamepad(iGamePad);
261}
262
263C4GamePadOpener::~C4GamePadOpener()
264{
265 if (C4GamePadControl::pInstance)
266 C4GamePadControl::pInstance->CloseGamepad(iGamePad);
267}
268
269void C4GamePadOpener::SetGamePad(int iNewGamePad)
270{
271 if (iNewGamePad == iGamePad) return;
272 assert(C4GamePadControl::pInstance);
273 C4GamePadControl::pInstance->CloseGamepad(iGamePad);
274 C4GamePadControl::pInstance->OpenGamepad(iGamePad = iNewGamePad);
275}
276
277#elif defined(USE_SDL_FOR_GAMEPAD)
278
279#include <SDL.h>
280#include <stdexcept>
281
282C4GamePadControl::C4GamePadControl()
283{
284 // Initialize SDL, if necessary.
285 try
286 {
287 sdlJoystickSubSys.emplace(SDL_INIT_JOYSTICK);
288 }
289 catch (const std::runtime_error &e)
290 {
291 LogNTr(spdlog::level::err, "SDL: {}", e.what());
292 // TODO: Handle
293 throw;
294 }
295 SDL_JoystickEventState(SDL_ENABLE);
296 if (!SDL_NumJoysticks()) LogNTr("No Gamepad found");
297}
298
299C4GamePadControl::~C4GamePadControl() {}
300
301void C4GamePadControl::Execute()
302{
303#ifndef USE_SDL_MAINLOOP
304 SDL_Event event;
305 while (SDL_PollEvent(&event))
306 {
307 switch (event.type)
308 {
309 case SDL_JOYAXISMOTION:
310 case SDL_JOYBALLMOTION:
311 case SDL_JOYHATMOTION:
312 case SDL_JOYBUTTONDOWN:
313 case SDL_JOYBUTTONUP:
314 FeedEvent(event);
315 break;
316 }
317 }
318#endif
319}
320
321namespace
322{
323 const int deadZone = 13337;
324
325 int amplify(int i)
326 {
327 if (i < 0)
328 return -(deadZone + 1);
329 if (i > 0)
330 return deadZone + 1;
331 return 0;
332 }
333}
334
335void C4GamePadControl::FeedEvent(SDL_Event &event)
336{
337 switch (event.type)
338 {
339 case SDL_JOYHATMOTION:
340 {
341 SDL_Event fakeX;
342 fakeX.jaxis.type = SDL_JOYAXISMOTION;
343 fakeX.jaxis.which = event.jhat.which;
344 fakeX.jaxis.axis = event.jhat.hat * 2 + 6; /* *magic*number* */
345 fakeX.jaxis.value = 0;
346 SDL_Event fakeY = fakeX;
347 fakeY.jaxis.axis += 1;
348 switch (event.jhat.value)
349 {
350 case SDL_HAT_LEFTUP: fakeX.jaxis.value = amplify(-1); fakeY.jaxis.value = amplify(-1); break;
351 case SDL_HAT_LEFT: fakeX.jaxis.value = amplify(-1); break;
352 case SDL_HAT_LEFTDOWN: fakeX.jaxis.value = amplify(-1); fakeY.jaxis.value = amplify(+1); break;
353 case SDL_HAT_UP: fakeY.jaxis.value = amplify(-1); break;
354 case SDL_HAT_DOWN: fakeY.jaxis.value = amplify(+1); break;
355 case SDL_HAT_RIGHTUP: fakeX.jaxis.value = amplify(+1); fakeY.jaxis.value = amplify(-1); break;
356 case SDL_HAT_RIGHT: fakeX.jaxis.value = amplify(+1); break;
357 case SDL_HAT_RIGHTDOWN: fakeX.jaxis.value = amplify(+1); fakeY.jaxis.value = amplify(+1); break;
358 }
359 FeedEvent(fakeX);
360 FeedEvent(fakeY);
361 return;
362 }
363 case SDL_JOYBALLMOTION:
364 {
365 SDL_Event fake;
366 fake.jaxis.type = SDL_JOYAXISMOTION;
367 fake.jaxis.which = event.jball.which;
368 fake.jaxis.axis = event.jball.ball * 2 + 12; /* *magic*number* */
369 fake.jaxis.value = amplify(event.jball.xrel);
370 FeedEvent(event);
371 fake.jaxis.axis += 1;
372 fake.jaxis.value = amplify(event.jball.yrel);
373 FeedEvent(event);
374 return;
375 }
376 case SDL_JOYAXISMOTION:
377 {
378 C4KeyCode minCode = KEY_Gamepad(event.jaxis.which, KEY_JOY_Axis(event.jaxis.axis, false));
379 C4KeyCode maxCode = KEY_Gamepad(event.jaxis.which, KEY_JOY_Axis(event.jaxis.axis, true));
380
381 // FIXME: This assumes that the axis really rests around (0, 0) if it is not used, which is not always true.
382 if (event.jaxis.value < -deadZone)
383 {
384 if (PressedAxis.count(minCode) == 0)
385 {
386 Game.DoKeyboardInput(
387 KEY_Gamepad(event.jaxis.which, minCode),
388 KEYEV_Down, false, false, false, false);
389 PressedAxis.insert(minCode);
390 }
391 }
392 else
393 {
394 if (PressedAxis.count(minCode) != 0)
395 {
396 Game.DoKeyboardInput(
397 KEY_Gamepad(event.jaxis.which, minCode),
398 KEYEV_Up, false, false, false, false);
399 PressedAxis.erase(minCode);
400 }
401 }
402 if (event.jaxis.value > +deadZone)
403 {
404 if (PressedAxis.count(maxCode) == 0)
405 {
406 Game.DoKeyboardInput(
407 KEY_Gamepad(event.jaxis.which, maxCode),
408 KEYEV_Down, false, false, false, false);
409 PressedAxis.insert(maxCode);
410 }
411 }
412 else
413 {
414 if (PressedAxis.count(maxCode) != 0)
415 {
416 Game.DoKeyboardInput(
417 KEY_Gamepad(event.jaxis.which, maxCode),
418 KEYEV_Up, false, false, false, false);
419 PressedAxis.erase(maxCode);
420 }
421 }
422 break;
423 }
424 case SDL_JOYBUTTONDOWN:
425 Game.DoKeyboardInput(
426 KEY_Gamepad(event.jbutton.which, KEY_JOY_Button(event.jbutton.button)),
427 KEYEV_Down, false, false, false, false);
428 break;
429 case SDL_JOYBUTTONUP:
430 Game.DoKeyboardInput(
431 KEY_Gamepad(event.jbutton.which, KEY_JOY_Button(event.jbutton.button)),
432 KEYEV_Up, false, false, false, false);
433 break;
434 }
435}
436
437int C4GamePadControl::GetGamePadCount()
438{
439 return (SDL_NumJoysticks());
440}
441
442C4GamePadOpener::C4GamePadOpener(int iGamepad)
443{
444 Joy = SDL_JoystickOpen(iGamepad);
445 if (!Joy) LogNTr(spdlog::level::err, "SDL: {}", SDL_GetError());
446}
447
448C4GamePadOpener::~C4GamePadOpener()
449{
450 if (Joy) SDL_JoystickClose(Joy);
451}
452
453void C4GamePadOpener::SetGamePad(int iGamepad)
454{
455 if (Joy)
456 SDL_JoystickClose(Joy);
457 Joy = SDL_JoystickOpen(iGamepad);
458 if (!Joy)
459 LogNTr(spdlog::level::err, "SDL: {}", SDL_GetError());
460}
461
462#else
463
464// Dedicated server and everything else with neither Win32 nor SDL.
465
466C4GamePadControl::C4GamePadControl() { LogNTr(level: spdlog::level::warn, message: "Engine without Gamepad support"); }
467C4GamePadControl::~C4GamePadControl() {}
468void C4GamePadControl::Execute() {}
469int C4GamePadControl::GetGamePadCount() { return 0; }
470
471C4GamePadOpener::C4GamePadOpener(int iGamepad) {}
472C4GamePadOpener::~C4GamePadOpener() {}
473void C4GamePadOpener::SetGamePad(int iGamepad) {}
474
475#endif // _WIN32
476