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