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