openMSX
Joystick.cc
Go to the documentation of this file.
1#include "Joystick.hh"
3#include "PlugException.hh"
6#include "Event.hh"
8#include "StateChange.hh"
9#include "TclObject.hh"
10#include "GlobalSettings.hh"
11#include "IntegerSetting.hh"
12#include "CommandController.hh"
13#include "CommandException.hh"
14#include "narrow.hh"
15#include "serialize.hh"
16#include "serialize_meta.hh"
17#include "StringOp.hh"
18#include "one_of.hh"
19#include "xrange.hh"
20#include <memory>
21
22namespace openmsx {
23
25 StateChangeDistributor& stateChangeDistributor,
26 CommandController& commandController,
27 GlobalSettings& globalSettings,
28 PluggingController& controller)
29{
30#ifdef SDL_JOYSTICK_DISABLED
31 (void)eventDistributor;
32 (void)stateChangeDistributor;
33 (void)commandController;
34 (void)globalSettings;
35 (void)controller;
36#else
37 for (auto i : xrange(SDL_NumJoysticks())) {
38 if (SDL_Joystick* joystick = SDL_JoystickOpen(i)) {
39 // Avoid devices that have axes but no buttons, like accelerometers.
40 // SDL 1.2.14 in Linux has an issue where it rejects a device from
41 // /dev/input/event* if it has no buttons but does not reject a
42 // device from /dev/input/js* if it has no buttons, while
43 // accelerometers do end up being symlinked as a joystick in
44 // practice.
45 if (InputEventGenerator::joystickNumButtons(joystick) != 0) {
46 controller.registerPluggable(
47 std::make_unique<Joystick>(
48 eventDistributor,
49 stateChangeDistributor,
50 commandController,
51 globalSettings,
52 joystick));
53 }
54 }
55 }
56#endif
57}
58
59
60class JoyState final : public StateChange
61{
62public:
63 JoyState() = default; // for serialize
64 JoyState(EmuTime::param time_, int joyNum_, uint8_t press_, uint8_t release_)
65 : StateChange(time_)
66 , joyNum(joyNum_), press(press_), release(release_)
67 {
68 assert((press != 0) || (release != 0));
69 assert((press & release) == 0);
70 }
71 [[nodiscard]] int getJoystick() const { return joyNum; }
72 [[nodiscard]] uint8_t getPress() const { return press; }
73 [[nodiscard]] uint8_t getRelease() const { return release; }
74
75 template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
76 {
77 ar.template serializeBase<StateChange>(*this);
78 ar.serialize("joyNum", joyNum,
79 "press", press,
80 "release", release);
81 }
82private:
83 int joyNum;
84 uint8_t press, release;
85};
87
88
89#ifndef SDL_JOYSTICK_DISABLED
90static void checkJoystickConfig(Interpreter& interp, TclObject& newValue)
91{
92 unsigned n = newValue.getListLength(interp);
93 if (n & 1) {
94 throw CommandException("Need an even number of elements");
95 }
96 for (unsigned i = 0; i < n; i += 2) {
97 std::string_view key = newValue.getListIndex(interp, i + 0).getString();
98 TclObject value = newValue.getListIndex(interp, i + 1);
99 if (key != one_of("A", "B", "LEFT", "RIGHT", "UP", "DOWN")) {
100 throw CommandException(
101 "Invalid MSX joystick action: must be one of "
102 "'A', 'B', 'LEFT', 'RIGHT', 'UP', 'DOWN'.");
103 }
104 for (auto j : xrange(value.getListLength(interp))) {
105 std::string_view host = value.getListIndex(interp, j).getString();
106 if (!host.starts_with("button") &&
107 !host.starts_with("+axis") &&
108 !host.starts_with("-axis") &&
109 !host.starts_with("L_hat") &&
110 !host.starts_with("R_hat") &&
111 !host.starts_with("U_hat") &&
112 !host.starts_with("D_hat")) {
113 throw CommandException(
114 "Invalid host joystick action: must be "
115 "one of 'button<N>', '+axis<N>', '-axis<N>', "
116 "'L_hat<N>', 'R_hat<N>', 'U_hat<N>', 'D_hat<N>'");
117 }
118 }
119 }
120}
121
122[[nodiscard]] static std::string getJoystickName(int joyNum)
123{
124 return strCat("joystick", narrow<char>('1' + joyNum));
125}
126
127[[nodiscard]] static TclObject getConfigValue(SDL_Joystick* joystick)
128{
129 TclObject listA, listB;
130 for (auto i : xrange(InputEventGenerator::joystickNumButtons(joystick))) {
131 auto button = tmpStrCat("button", i);
132 //std::string_view button = tmp;
133 if (i & 1) {
134 listB.addListElement(button);
135 } else {
136 listA.addListElement(button);
137 }
138 }
139 TclObject value;
140 value.addDictKeyValues("LEFT", makeTclList("-axis0", "L_hat0"),
141 "RIGHT", makeTclList("+axis0", "R_hat0"),
142 "UP", makeTclList("-axis1", "U_hat0"),
143 "DOWN", makeTclList("+axis1", "D_hat0"),
144 "A", listA,
145 "B", listB);
146 return value;
147}
148
149// Note: It's OK to open/close the same SDL_Joystick multiple times (we open it
150// once per MSX machine). The SDL documentation doesn't state this, but I
151// checked the implementation and a SDL_Joystick uses a 'reference count' on
152// the open/close calls.
154 StateChangeDistributor& stateChangeDistributor_,
155 CommandController& commandController,
156 GlobalSettings& globalSettings,
157 SDL_Joystick* joystick_)
158 : eventDistributor(eventDistributor_)
159 , stateChangeDistributor(stateChangeDistributor_)
160 , joystick(joystick_)
161 , joyNum(SDL_JoystickInstanceID(joystick_))
162 , deadSetting(globalSettings.getJoyDeadZoneSetting(joyNum))
163 , name(getJoystickName(joyNum))
164 , desc(SDL_JoystickName(joystick_))
165 , configSetting(commandController, tmpStrCat(name, "_config"),
166 "joystick configuration", getConfigValue(joystick).getString())
167{
168 auto& interp = commandController.getInterpreter();
169 configSetting.setChecker([&interp](TclObject& newValue) {
170 checkJoystickConfig(interp, newValue); });
171}
172
174{
175 if (isPluggedIn()) {
176 Joystick::unplugHelper(EmuTime::dummy());
177 }
178 if (joystick) {
179 SDL_JoystickClose(joystick);
180 }
181}
182
183// Pluggable
184std::string_view Joystick::getName() const
185{
186 return name;
187}
188
189std::string_view Joystick::getDescription() const
190{
191 return desc;
192}
193
194void Joystick::plugHelper(Connector& /*connector*/, EmuTime::param /*time*/)
195{
196 if (!joystick) {
197 throw PlugException("Failed to open joystick device");
198 }
199 plugHelper2();
200 status = calcState();
201}
202
203void Joystick::plugHelper2()
204{
205 eventDistributor.registerEventListener(*this);
206 stateChangeDistributor.registerListener(*this);
207}
208
209void Joystick::unplugHelper(EmuTime::param /*time*/)
210{
211 stateChangeDistributor.unregisterListener(*this);
212 eventDistributor.unregisterEventListener(*this);
213}
214
215
216// JoystickDevice
217uint8_t Joystick::read(EmuTime::param /*time*/)
218{
219 return pin8 ? 0x3F : status;
220}
221
222void Joystick::write(uint8_t value, EmuTime::param /*time*/)
223{
224 pin8 = (value & 0x04) != 0;
225}
226
227uint8_t Joystick::calcState()
228{
229 uint8_t result = JOY_UP | JOY_DOWN | JOY_LEFT | JOY_RIGHT |
231 if (joystick) {
232 int threshold = (deadSetting.getInt() * 32768) / 100;
233 auto& interp = configSetting.getInterpreter();
234 const auto& dict = configSetting.getValue();
235 if (getState(interp, dict, "A" , threshold)) result &= ~JOY_BUTTONA;
236 if (getState(interp, dict, "B" , threshold)) result &= ~JOY_BUTTONB;
237 if (getState(interp, dict, "UP" , threshold)) result &= ~JOY_UP;
238 if (getState(interp, dict, "DOWN" , threshold)) result &= ~JOY_DOWN;
239 if (getState(interp, dict, "LEFT" , threshold)) result &= ~JOY_LEFT;
240 if (getState(interp, dict, "RIGHT", threshold)) result &= ~JOY_RIGHT;
241 }
242 return result;
243}
244
245bool Joystick::getState(Interpreter& interp, const TclObject& dict,
246 std::string_view key, int threshold)
247{
248 try {
249 const auto& list = dict.getDictValue(interp, key);
250 for (auto i : xrange(list.getListLength(interp))) {
251 const auto& elem = list.getListIndex(interp, i).getString();
252 if (elem.starts_with("button")) {
253 if (auto n = StringOp::stringToBase<10, int>(elem.substr(6))) {
254 if (InputEventGenerator::joystickGetButton(joystick, *n)) {
255 return true;
256 }
257 }
258 } else if (elem.starts_with("+axis")) {
259 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
260 if (SDL_JoystickGetAxis(joystick, *n) > threshold) {
261 return true;
262 }
263 }
264 } else if (elem.starts_with("-axis")) {
265 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
266 if (SDL_JoystickGetAxis(joystick, *n) < -threshold) {
267 return true;
268 }
269 }
270 } else if (elem.starts_with("L_hat")) {
271 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
272 if (SDL_JoystickGetHat(joystick, *n) & SDL_HAT_LEFT) {
273 return true;
274 }
275 }
276 } else if (elem.starts_with("R_hat")) {
277 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
278 if (SDL_JoystickGetHat(joystick, *n) & SDL_HAT_RIGHT) {
279 return true;
280 }
281 }
282 } else if (elem.starts_with("U_hat")) {
283 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
284 if (SDL_JoystickGetHat(joystick, *n) & SDL_HAT_UP) {
285 return true;
286 }
287 }
288 } else if (elem.starts_with("D_hat")) {
289 if (auto n = StringOp::stringToBase<10, int>(elem.substr(5))) {
290 if (SDL_JoystickGetHat(joystick, *n) & SDL_HAT_DOWN) {
291 return true;
292 }
293 }
294 }
295 }
296 } catch (...) {
297 // Error, in getListLength() or getListIndex().
298 // In either case we can't do anything about it here, so ignore.
299 }
300 return false;
301}
302
303// MSXEventListener
304void Joystick::signalMSXEvent(const Event& event,
305 EmuTime::param time) noexcept
306{
307 const auto* joyEvent = get_if<JoystickEvent>(event);
308 if (!joyEvent) return;
309
310 // TODO: It would be more efficient to make a dispatcher instead of
311 // sending the event to all joysticks.
312 if (joyEvent->getJoystick() != joyNum) return;
313
314 // TODO: Currently this recalculates the whole joystick state. It might
315 // be possible to implement this more efficiently by using the specific
316 // event information. Though that's not trivial because e.g. multiple
317 // host buttons can map to the same MSX button. Also calcState()
318 // involves some string processing. It might be possible to only parse
319 // the config once (per setting change). Though this solution is likely
320 // good enough.
321 createEvent(time, calcState());
322}
323
324void Joystick::createEvent(EmuTime::param time, uint8_t newStatus)
325{
326 uint8_t diff = status ^ newStatus;
327 if (!diff) {
328 // event won't actually change the status, so ignore it
329 return;
330 }
331 // make sure we create an event with minimal changes
332 uint8_t press = status & diff;
333 uint8_t release = newStatus & diff;
334 stateChangeDistributor.distributeNew<JoyState>(
335 time, joyNum, press, release);
336}
337
338// StateChangeListener
339void Joystick::signalStateChange(const StateChange& event)
340{
341 const auto* js = dynamic_cast<const JoyState*>(&event);
342 if (!js) return;
343
344 // TODO: It would be more efficient to make a dispatcher instead of
345 // sending the event to all joysticks.
346 // TODO an alternative is to log events based on the connector instead
347 // of the joystick. That would make it possible to replay on a
348 // different host without an actual SDL joystick connected.
349 if (js->getJoystick() != joyNum) return;
350
351 status = (status & ~js->getPress()) | js->getRelease();
352}
353
354void Joystick::stopReplay(EmuTime::param time) noexcept
355{
356 createEvent(time, calcState());
357}
358
359// version 1: Initial version, the variable status was not serialized.
360// version 2: Also serialize the above variable, this is required for
361// record/replay, see comment in Keyboard.cc for more details.
362template<typename Archive>
363void Joystick::serialize(Archive& ar, unsigned version)
364{
365 if (ar.versionAtLeast(version, 2)) {
366 ar.serialize("status", status);
367 }
368 if constexpr (Archive::IS_LOADER) {
369 if (joystick && isPluggedIn()) {
370 plugHelper2();
371 }
372 }
373 // no need to serialize 'pin8' it's automatically restored via write()
374}
377
378#endif // SDL_JOYSTICK_DISABLED
379
380} // namespace openmsx
Definition: one_of.hh:7
virtual Interpreter & getInterpreter()=0
Represents something you can plug devices into.
Definition: Connector.hh:21
This class contains settings that are used by several other class (including some singletons).
static int joystickNumButtons(SDL_Joystick *joystick)
Normally the following two functions simply delegate to SDL_JoystickNumButtons() and SDL_JoystickGetB...
static bool joystickGetButton(SDL_Joystick *joystick, int button)
int getInt() const noexcept
int getJoystick() const
Definition: Joystick.cc:71
uint8_t getRelease() const
Definition: Joystick.cc:73
uint8_t getPress() const
Definition: Joystick.cc:72
void serialize(Archive &ar, unsigned)
Definition: Joystick.cc:75
JoyState()=default
JoyState(EmuTime::param time_, int joyNum_, uint8_t press_, uint8_t release_)
Definition: Joystick.cc:64
static constexpr int JOY_BUTTONA
static constexpr int JOY_RIGHT
static constexpr int JOY_LEFT
static constexpr int JOY_DOWN
static constexpr int JOY_BUTTONB
static constexpr int JOY_UP
Uses an SDL joystick to emulate an MSX joystick.
Definition: Joystick.hh:28
void write(uint8_t value, EmuTime::param time) override
Write a value to the joystick device.
Definition: Joystick.cc:222
std::string_view getName() const override
Name used to identify this pluggable.
Definition: Joystick.cc:184
void unplugHelper(EmuTime::param time) override
Definition: Joystick.cc:209
std::string_view getDescription() const override
Description for this pluggable.
Definition: Joystick.cc:189
uint8_t read(EmuTime::param time) override
Read from the joystick device.
Definition: Joystick.cc:217
~Joystick() override
Definition: Joystick.cc:173
void plugHelper(Connector &connector, EmuTime::param time) override
Definition: Joystick.cc:194
static void registerAll(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, CommandController &commandController, GlobalSettings &globalSettings, PluggingController &controller)
Register all available SDL joysticks.
Definition: Joystick.cc:24
void serialize(Archive &ar, unsigned version)
Definition: Joystick.cc:363
Joystick(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, CommandController &commandController, GlobalSettings &globalSettings, SDL_Joystick *joystick)
Definition: Joystick.cc:153
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
void unregisterEventListener(MSXEventListener &listener)
Unregisters a previously registered event listener.
Thrown when a plug action fails.
bool isPluggedIn() const
Returns true if this pluggable is currently plugged into a connector.
Definition: Pluggable.hh:49
Central administration of Connectors and Pluggables.
void registerPluggable(std::unique_ptr< Pluggable > pluggable)
Add a Pluggable to the registry.
void setChecker(std::function< void(TclObject &)> checkFunc_)
Set value-check-callback.
Definition: Setting.hh:160
Interpreter & getInterpreter() const
Definition: Setting.cc:148
const TclObject & getValue() const final
Gets the current value of this setting as a TclObject.
Definition: Setting.hh:142
void registerListener(StateChangeListener &listener)
(Un)registers the given object to receive state change events.
void distributeNew(EmuTime::param time, Args &&...args)
Deliver the event to all registered listeners MSX input devices should call the distributeNew() versi...
void unregisterListener(StateChangeListener &listener)
Base class for all external MSX state changing events.
Definition: StateChange.hh:20
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:134
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition: TclObject.cc:152
zstring_view getString() const
Definition: TclObject.cc:120
This file implemented 3 utility functions:
Definition: Autofire.cc:9
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
REGISTER_POLYMORPHIC_CLASS(StateChange, AutofireStateChange, "AutofireStateChange")
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:283
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:1021
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:610
std::string strCat(Ts &&...ts)
Definition: strCat.hh:542
constexpr auto xrange(T e)
Definition: xrange.hh:133