OXYGENHUB
◆ main openmsx / src / input / JoystickManager.cc
7.1 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "JoystickManager.hh"
2
3
#include "CommandException.hh"
4
#include "FloatSetting.hh"
5
#include "Reactor.hh"
6
7
#include "narrow.hh"
8
#include "outer.hh"
9
#include "strCat.hh"
10
11
#include <algorithm>
12
#include <cassert>
13
#include <cmath>
14
15
namespace openmsx {
16
17
JoystickManager::JoystickManager(Reactor& reactor)
18
	: commandController(reactor.getCommandController())
19
	, joystickInfo(reactor.getOpenMSXInfoCommand())
20
{
21
	// Note: we don't explicitly enumerate all joysticks which are already
22
	// present at startup. Instead we rely on SDL_JOYDEVICEADDED events:
23
	// these also get send for the initial joysticks.
24
25
	// joysticks generate events
26
	SDL_JoystickEventState(SDL_ENABLE); // is this needed?
27
}
28
29
JoystickManager::~JoystickManager()
30
{
31
	for (auto& info : infos) {
32
		if (info.joystick) SDL_JoystickClose(info.joystick);
33
	}
34
}
35
36
size_t JoystickManager::getFreeSlot()
37
{
38
	if (auto it = std::ranges::find(infos, nullptr, &Info::joystick); it != infos.end()) {
39
		return std::distance(infos.begin(), it);
40
	}
41
	auto idx = infos.size();
42
	infos.emplace_back();
43
	auto& info = infos[idx];
44
	info.deadZoneSetting = std::make_unique<FloatSetting>(
45
		commandController,
46
		tmpStrCat("joystick", idx + 1, "_deadzone"),
47
		"(as a percentage) inputs below this threshold are clipped to zero (dead)",
48
		25.0f, 0.0f, 100.0f);
49
	info.midPointSetting = std::make_unique<FloatSetting>(
50
		commandController,
51
		tmpStrCat("joystick", idx + 1, "_midpoint"),
52
		"(as a percentage) this input gets mapped to half-strength output, allows to select a non-linear curve",
53
		75.0f, 0.0f, 100.0f);
54
	info.saturationSetting = std::make_unique<FloatSetting>(
55
		commandController,
56
		tmpStrCat("joystick", idx + 1, "_saturation"),
57
		"(as a percentage) inputs above this threshold get mapped to max-output",
58
		90.0f, 0.0f, 100.0f);
59
	return idx;
60
}
61
62
void JoystickManager::add(int deviceIndex)
63
{
64
	auto idx = getFreeSlot();
65
	auto& info = infos[idx];
66
	info.joystick = SDL_JoystickOpen(deviceIndex);
67
	info.instanceId = info.joystick ? SDL_JoystickInstanceID(info.joystick) : -1;
68
}
69
70
void JoystickManager::remove(int instanceId)
71
{
72
	auto it = std::ranges::find(infos, instanceId, &Info::instanceId);
73
	if (it == infos.end()) return; // this shouldn't happen
74
75
	SDL_JoystickClose(it->joystick);
76
	it->joystick = nullptr;
77
	it->instanceId = -1;
78
}
79
80
std::vector<JoystickId> JoystickManager::getConnectedJoysticks() const
81
{
82
	// TODO c++20 std::views::filter | std::views::transform
83
	std::vector<JoystickId> result;
84
	for (auto i : xrange(infos.size())) {
85
		if (!infos[i].joystick) continue;
86
		result.emplace_back(unsigned(i));
87
	}
88
	return result;
89
}
90
91
int JoystickManager::getJoyDeadThreshold(JoystickId joyId) const
92
{
93
	unsigned id = joyId.raw();
94
	return (id < infos.size()) ? int(infos[id].deadZoneSetting->getFloat() * (32768.0f / 100.0f)) : 8192;
95
}
96
97
float JoystickManager::getJoyValue(JoystickId joyId, const JoystickAxisMotionEvent& e) const
98
{
99
	unsigned id = joyId.raw();
100
	if (id >= infos.size()) return 0.0f;
101
102
	auto rawValue = float(e.getValue()); // [-32768, 32767]
103
	auto value = std::abs(rawValue) * (1.0f / 32768.0f);
104
105
	const auto& info = infos[id];
106
	auto inner = info.deadZoneSetting->getFloat() * (1.0f / 100.0f);
107
	if (value <= inner) return 0.0f;
108
	auto outer = info.saturationSetting->getFloat() * (1.0f / 100.0f);
109
	if (value >= outer) return std::copysign(1.0f, rawValue);
110
111
	assert(inner < outer);
112
	auto middle = info.midPointSetting->getFloat() * (1.0f / 100.0f);
113
	middle = std::clamp(middle, inner, outer);
114
	auto f = std::log(0.5f) / std::log((middle - inner) / (outer - inner));
115
	auto output = std::pow((value - inner) / (outer - inner), f);
116
	return std::copysign(output, rawValue);
117
}
118
119
JoystickManager::Info* JoystickManager::getSettings(JoystickId joyId)
120
{
121
	unsigned id = joyId.raw();
122
	return (id < infos.size()) ? &infos[id] : nullptr;
123
}
124
125
std::string JoystickManager::getDisplayName(JoystickId joyId) const
126
{
127
	unsigned id = joyId.raw();
128
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
129
	return joystick ? SDL_JoystickName(joystick) : "[Not plugged in]";
130
}
131
132
std::optional<unsigned> JoystickManager::getNumAxes(JoystickId joyId) const
133
{
134
	unsigned id = joyId.raw();
135
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
136
	if (!joystick) return {};
137
	return SDL_JoystickNumAxes(joystick);
138
}
139
140
std::optional<unsigned> JoystickManager::getNumBalls(JoystickId joyId) const
141
{
142
	unsigned id = joyId.raw();
143
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
144
	if (!joystick) return {};
145
	return SDL_JoystickNumBalls(joystick);
146
}
147
148
std::optional<unsigned> JoystickManager::getNumButtons(JoystickId joyId) const
149
{
150
	unsigned id = joyId.raw();
151
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
152
	if (!joystick) return {};
153
	return SDL_JoystickNumButtons(joystick);
154
}
155
156
std::optional<unsigned> JoystickManager::getNumHats(JoystickId joyId) const
157
{
158
	unsigned id = joyId.raw();
159
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
160
	if (!joystick) return {};
161
	return SDL_JoystickNumHats(joystick);
162
}
163
164
std::optional<int16_t> JoystickManager::getAxis(JoystickId joyId, int axis) const
165
{
166
	unsigned id = joyId.raw();
167
	auto* joystick = (id < infos.size()) ? infos[id].joystick : nullptr;
168
	if (!joystick) return {};
169
	return SDL_JoystickGetAxis(joystick, axis);
170
}
171
172
std::optional<JoystickId> JoystickManager::translateSdlInstanceId(SDL_Event& evt) const
173
{
174
	int instanceId = evt.jbutton.which; // any SDL joystick event
175
	auto it = std::ranges::find(infos, instanceId, &Info::instanceId);
176
	if (it == infos.end()) return {}; // should not happen
177
	evt.jbutton.which = narrow<int>(std::distance(infos.begin(), it));
178
	return JoystickId(evt.jbutton.which);
179
}
180
181
182
// JoystickInfo
183
184
JoystickManager::JoystickInfo::JoystickInfo(InfoCommand& openMsxInfoCommand)
185
	: InfoTopic(openMsxInfoCommand, "joystick")
186
{
187
}
188
189
void JoystickManager::JoystickInfo::execute(std::span<const TclObject> tokens, TclObject& result) const
190
{
191
	checkNumArgs(tokens, Between{2, 3}, Prefix{2}, "?joystick-id?");
192
	auto& manager = OUTER(JoystickManager, joystickInfo);
193
	switch (tokens.size()) {
194
	case 2:
195
		for (auto i : xrange(manager.infos.size())) {
196
			if (!manager.infos[i].joystick) continue;
197
			result.addListElement(JoystickId(unsigned(i)).str());
198
		}
199
		break;
200
	case 3: {
201
		auto str = tokens[2].getString();
202
		auto id = JoystickId::parse(str);
203
		if (!id) {
204
			throw CommandException("Invalid host joystick ID: ", str);
205
		}
206
		result.addDictKeyValue("name", manager.getDisplayName(*id));
207
		if (auto n = manager.getNumAxes(*id)) {
208
			result.addDictKeyValue("axes", *n);
209
		}
210
		if (auto n = manager.getNumBalls(*id)) {
211
			result.addDictKeyValue("balls", *n);
212
		}
213
		if (auto n = manager.getNumButtons(*id)) {
214
			result.addDictKeyValue("buttons", *n);
215
		}
216
		if (auto n = manager.getNumHats(*id)) {
217
			result.addDictKeyValue("hats", *n);
218
		}
219
		break;
220
	}
221
	}
222
}
223
224
std::string JoystickManager::JoystickInfo::help(std::span<const TclObject> /*tokens*/) const
225
{
226
	return "Without argument: show list of available host joystick-IDs.\n"
227
	       "With a joystick-ID argument: return a dict (key-values pairs) with extra info, for example:\n"
228
	       "  the 'name' key contains the name for the given host joystick\n"
229
	       "  'axes' / 'buttons' contains the number of axes / buttons this joystick has\n";
230
}
231
232
} // namespace openmsx