openMSX
Joystick.cc
Go to the documentation of this file.
1 #include "Joystick.hh"
2 #include "PluggingController.hh"
3 #include "PlugException.hh"
4 #include "MSXEventDistributor.hh"
6 #include "Event.hh"
7 #include "InputEventGenerator.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 "serialize.hh"
15 #include "serialize_meta.hh"
16 #include "StringOp.hh"
17 #include "one_of.hh"
18 #include "xrange.hh"
19 #include <memory>
20 
21 namespace openmsx {
22 
24  StateChangeDistributor& stateChangeDistributor,
25  CommandController& commandController,
26  GlobalSettings& globalSettings,
27  PluggingController& controller)
28 {
29 #ifdef SDL_JOYSTICK_DISABLED
30  (void)eventDistributor;
31  (void)stateChangeDistributor;
32  (void)commandController;
33  (void)globalSettings;
34  (void)controller;
35 #else
36  for (auto i : xrange(SDL_NumJoysticks())) {
37  if (SDL_Joystick* joystick = SDL_JoystickOpen(i)) {
38  // Avoid devices that have axes but no buttons, like accelerometers.
39  // SDL 1.2.14 in Linux has an issue where it rejects a device from
40  // /dev/input/event* if it has no buttons but does not reject a
41  // device from /dev/input/js* if it has no buttons, while
42  // accelerometers do end up being symlinked as a joystick in
43  // practice.
44  if (InputEventGenerator::joystickNumButtons(joystick) != 0) {
45  controller.registerPluggable(
46  std::make_unique<Joystick>(
47  eventDistributor,
48  stateChangeDistributor,
49  commandController,
50  globalSettings,
51  joystick));
52  }
53  }
54  }
55 #endif
56 }
57 
58 
59 class JoyState final : public StateChange
60 {
61 public:
62  JoyState() = default; // for serialize
63  JoyState(EmuTime::param time_, unsigned joyNum_, byte press_, byte release_)
64  : StateChange(time_)
65  , joyNum(joyNum_), press(press_), release(release_)
66  {
67  assert((press != 0) || (release != 0));
68  assert((press & release) == 0);
69  }
70  [[nodiscard]] unsigned getJoystick() const { return joyNum; }
71  [[nodiscard]] byte getPress() const { return press; }
72  [[nodiscard]] byte getRelease() const { return release; }
73 
74  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
75  {
76  ar.template serializeBase<StateChange>(*this);
77  ar.serialize("joyNum", joyNum,
78  "press", press,
79  "release", release);
80  }
81 private:
82  unsigned joyNum;
83  byte press, release;
84 };
86 
87 
88 #ifndef SDL_JOYSTICK_DISABLED
89 static void checkJoystickConfig(Interpreter& interp, TclObject& newValue)
90 {
91  unsigned n = newValue.getListLength(interp);
92  if (n & 1) {
93  throw CommandException("Need an even number of elements");
94  }
95  for (unsigned i = 0; i < n; i += 2) {
96  std::string_view key = newValue.getListIndex(interp, i + 0).getString();
97  TclObject value = newValue.getListIndex(interp, i + 1);
98  if (key != one_of("A", "B", "LEFT", "RIGHT", "UP", "DOWN")) {
99  throw CommandException(
100  "Invalid MSX joystick action: must be one of "
101  "'A', 'B', 'LEFT', 'RIGHT', 'UP', 'DOWN'.");
102  }
103  for (auto j : xrange(value.getListLength(interp))) {
104  std::string_view host = value.getListIndex(interp, j).getString();
105  if (!host.starts_with("button") &&
106  !host.starts_with("+axis") &&
107  !host.starts_with("-axis") &&
108  !host.starts_with("L_hat") &&
109  !host.starts_with("R_hat") &&
110  !host.starts_with("U_hat") &&
111  !host.starts_with("D_hat")) {
112  throw CommandException(
113  "Invalid host joystick action: must be "
114  "one of 'button<N>', '+axis<N>', '-axis<N>', "
115  "'L_hat<N>', 'R_hat<N>', 'U_hat<N>', 'D_hat<N>'");
116  }
117  }
118  }
119 }
120 
121 [[nodiscard]] static std::string getJoystickName(unsigned joyNum)
122 {
123  return strCat("joystick", char('1' + joyNum));
124 }
125 
126 [[nodiscard]] static TclObject getConfigValue(SDL_Joystick* joystick)
127 {
128  TclObject listA, listB;
129  for (auto i : xrange(InputEventGenerator::joystickNumButtons(joystick))) {
130  auto button = tmpStrCat("button", i);
131  //std::string_view button = tmp;
132  if (i & 1) {
133  listB.addListElement(button);
134  } else {
135  listA.addListElement(button);
136  }
137  }
138  TclObject value;
139  value.addDictKeyValues("LEFT", makeTclList("-axis0", "L_hat0"),
140  "RIGHT", makeTclList("+axis0", "R_hat0"),
141  "UP", makeTclList("-axis1", "U_hat0"),
142  "DOWN", makeTclList("+axis1", "D_hat0"),
143  "A", listA,
144  "B", listB);
145  return value;
146 }
147 
148 // Note: It's OK to open/close the same SDL_Joystick multiple times (we open it
149 // once per MSX machine). The SDL documentation doesn't state this, but I
150 // checked the implementation and a SDL_Joystick uses a 'reference count' on
151 // the open/close calls.
153  StateChangeDistributor& stateChangeDistributor_,
154  CommandController& commandController,
155  GlobalSettings& globalSettings,
156  SDL_Joystick* joystick_)
157  : eventDistributor(eventDistributor_)
158  , stateChangeDistributor(stateChangeDistributor_)
159  , joystick(joystick_)
160  , joyNum(SDL_JoystickInstanceID(joystick_))
161  , deadSetting(globalSettings.getJoyDeadzoneSetting(joyNum))
162  , name(getJoystickName(joyNum))
163  , desc(SDL_JoystickName(joystick_))
164  , configSetting(commandController, tmpStrCat(name, "_config"),
165  "joystick configuration", getConfigValue(joystick).getString())
166  , pin8(false) // avoid UMR
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
184 std::string_view Joystick::getName() const
185 {
186  return name;
187 }
188 
189 std::string_view Joystick::getDescription() const
190 {
191  return desc;
192 }
193 
194 void 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 
203 void Joystick::plugHelper2()
204 {
205  eventDistributor.registerEventListener(*this);
206  stateChangeDistributor.registerListener(*this);
207 }
208 
209 void Joystick::unplugHelper(EmuTime::param /*time*/)
210 {
211  stateChangeDistributor.unregisterListener(*this);
212  eventDistributor.unregisterEventListener(*this);
213 }
214 
215 
216 // JoystickDevice
217 byte Joystick::read(EmuTime::param /*time*/)
218 {
219  return pin8 ? 0x3F : status;
220 }
221 
222 void Joystick::write(byte value, EmuTime::param /*time*/)
223 {
224  pin8 = (value & 0x04) != 0;
225 }
226 
227 byte Joystick::calcState()
228 {
229  byte 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 
245 bool 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, unsigned>(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, unsigned>(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, unsigned>(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, unsigned>(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, unsigned>(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, unsigned>(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, unsigned>(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
304 void 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 
324 void Joystick::createEvent(EmuTime::param time, byte newStatus)
325 {
326  byte 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  byte press = status & diff;
333  byte release = newStatus & diff;
334  stateChangeDistributor.distributeNew<JoyState>(
335  time, joyNum, press, release);
336 }
337 
338 // StateChangeListener
339 void 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 
354 void 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.
362 template<typename Archive>
363 void 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
unsigned getJoystick() const
Definition: Joystick.cc:70
byte getRelease() const
Definition: Joystick.cc:72
void serialize(Archive &ar, unsigned)
Definition: Joystick.cc:74
JoyState(EmuTime::param time_, unsigned joyNum_, byte press_, byte release_)
Definition: Joystick.cc:63
JoyState()=default
byte getPress() const
Definition: Joystick.cc:71
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
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
~Joystick() override
Definition: Joystick.cc:173
void write(byte value, EmuTime::param time) override
Write a value to the joystick device.
Definition: Joystick.cc:222
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:23
void serialize(Archive &ar, unsigned version)
Definition: Joystick.cc:363
byte read(EmuTime::param time) override
Read from the joystick device.
Definition: Joystick.cc:217
Joystick(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, CommandController &commandController, GlobalSettings &globalSettings, SDL_Joystick *joystick)
Definition: Joystick.cc:152
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
const TclObject & getValue() const final
Gets the current value of this setting as a TclObject.
Definition: Setting.hh:142
Interpreter & getInterpreter() const
Definition: Setting.cc:148
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:125
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition: TclObject.cc:143
zstring_view getString() const
Definition: TclObject.cc:111
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:281
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:1009
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:617
std::string strCat(Ts &&...ts)
Definition: strCat.hh:549
constexpr auto xrange(T e)
Definition: xrange.hh:133