-
-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathSDLControllerManager.java
More file actions
312 lines (261 loc) · 13.2 KB
/
Copy pathSDLControllerManager.java
File metadata and controls
312 lines (261 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package dev.isxander.controlify.controllermanager;
import com.google.common.io.ByteStreams;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.config.settings.profile.ProfileSettings;
import dev.isxander.controlify.controller.info.ControllerInfo;
import dev.isxander.controlify.controller.id.ControllerType;
import dev.isxander.controlify.controller.ControllerEntity;
import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.driver.CompoundDriver;
import dev.isxander.controlify.driver.Driver;
import dev.isxander.controlify.driver.sdl.SDL3GamepadDriver;
import dev.isxander.controlify.driver.sdl.SDL3JoystickDriver;
import dev.isxander.controlify.driver.sdl.SDLNativesLoader;
import dev.isxander.controlify.driver.sdl.SDLUtil;
import dev.isxander.controlify.driver.steamdeck.SteamDeckDriver;
import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil;
import dev.isxander.controlify.hid.ControllerHIDService;
import dev.isxander.controlify.hid.ControllerSpecify;
import dev.isxander.controlify.hid.HIDDevice;
import dev.isxander.controlify.hid.HIDID;
import dev.isxander.controlify.utils.CUtil;
import dev.isxander.controlify.utils.ControllerUtils;
import dev.isxander.controlify.utils.log.ControlifyLogger;
import dev.isxander.sdl3java.api.events.SDL_EventFilter;
import dev.isxander.sdl3java.api.events.events.SDL_Event;
import dev.isxander.sdl3java.api.gamepad.SDL_Gamepad;
import dev.isxander.sdl3java.api.iostream.SDL_IOStream;
import dev.isxander.sdl3java.api.joystick.SDL_Joystick;
import dev.isxander.sdl3java.api.joystick.SDL_JoystickGUID;
import dev.isxander.sdl3java.api.joystick.SDL_JoystickID;
import dev.isxander.sdl3java.jna.size_t;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
import static dev.isxander.sdl3java.api.error.SdlError.*;
import static dev.isxander.sdl3java.api.events.SDL_EventType.*;
import static dev.isxander.sdl3java.api.events.SdlEvents.*;
import static dev.isxander.sdl3java.api.gamepad.SdlGamepad.*;
import static dev.isxander.sdl3java.api.iostream.SdlIOStream.*;
import static dev.isxander.sdl3java.api.joystick.SdlJoystick.*;
public class SDLControllerManager extends AbstractControllerManager {
private SDL_Event event = new SDL_Event();
// must keep a reference to prevent GC from collecting it and the callback failing
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final EventFilter eventFilter;
private boolean steamDeckConsumed = false;
public SDLControllerManager(ControlifyLogger logger) {
super(logger);
logger.debugLog("Controller manager using SDL3");
logger.validateIsTrue(SDLNativesLoader.isLoaded(), "SDL3 natives must be loaded before creating SDLControllerManager");
SDL_SetEventFilter(eventFilter = new EventFilter(), Pointer.NULL);
}
@Override
public void tick(boolean outOfFocus) {
if (event == null) {
logger.error("SDL_Event has somehow been set to null. Recreating...");
event = new SDL_Event();
}
while (SDL_PollEvent(event)) {
switch (event.type) {
// On added, `which` refers to the device index
case SDL_EVENT_JOYSTICK_ADDED -> {
SDL_JoystickID jid = event.jdevice.which;
logger.validateIsTrue(jid != null, "event.jdevice.which was null during SDL_EVENT_JOYSTICK_ADDED event");
if (!isAssignedToThisInstance(jid)) {
logger.debugLog("Ignoring hotplugged joystick {} - not assigned to this instance", jid.intValue());
break;
}
logger.debugLog("SDL event: Joystick added: {}", jid.intValue());
UniqueControllerID ucid = new SDLUniqueControllerID(jid);
Optional<ControllerEntity> controllerOpt = tryCreate(
ucid,
fetchTypeFromSDL(jid)
.orElse(new ControllerHIDService.ControllerHIDInfo(ControllerType.DEFAULT, Optional.empty()))
);
controllerOpt.ifPresent(controller -> {
ControllerUtils.wrapControllerError(() -> onControllerConnected(controller, true), "Connecting controller", controller);
});
}
// On removed, `which` refers to the device instance ID
case SDL_EVENT_JOYSTICK_REMOVED -> {
SDL_JoystickID jid = event.jdevice.which;
logger.validateIsTrue(jid != null, "event.jdevice.which was null during SDL_EVENT_JOYSTICK_REMOVED event");
logger.debugLog("SDL event: Joystick removed: {}", jid.intValue());
getController(new SDLUniqueControllerID(jid))
.ifPresentOrElse(
this::onControllerRemoved,
() -> CUtil.LOGGER.warn("Controller removed but not found: {}", jid.intValue())
);
}
}
}
super.tick(outOfFocus);
}
private boolean isAssignedToThisInstance(SDL_JoystickID jid) {
ControllerSpecify cfg = ControllerSpecify.get();
if (!cfg.isFilteringEnabled()) return true;
SDL_Joystick tempHandle = SDL_OpenJoystick(jid);
if (tempHandle == null) {
logger.warn("Could not open joystick {} to check serial: {}", jid.intValue(), SDL_GetError());
return false;
}
try {
String serial = SDL_GetJoystickSerial(tempHandle);
if (serial == null) {
logger.warn("Joystick {} reported no serial number - cannot filter by MAC for this device", jid.intValue());
return false;
}
return ControllerSpecify.normalizeMac(serial).equals(ControllerSpecify.normalizeMac(cfg.getAssignedControllerMac()));
} finally {
SDL_CloseJoystick(tempHandle);
}
}
@Override
public void discoverControllers() {
logger.debugLog("Discovering controllers...");
SDL_JoystickID[] joysticks = SDL_GetJoysticks();
for (SDL_JoystickID jid : joysticks) {
if (!isAssignedToThisInstance(jid)) {
logger.debugLog("Skipping joystick {} - not assigned to this instance", jid.intValue());
continue;
}
Optional<ControllerEntity> controllerOpt = tryCreate(
new SDLUniqueControllerID(jid),
fetchTypeFromSDL(jid)
.orElse(new ControllerHIDService.ControllerHIDInfo(ControllerType.DEFAULT, Optional.empty()))
);
controllerOpt.ifPresent(controller -> onControllerConnected(controller, false));
}
}
@Override
protected Optional<ControllerEntity> createController(UniqueControllerID ucid, ControllerHIDService.ControllerHIDInfo hidInfo, ControlifyLogger controllerLogger) {
SDL_JoystickID jid = ((SDLUniqueControllerID) ucid).jid();
controllerLogger.debugLog("Creating controller: {}", jid.intValue());
boolean isGamepad = isControllerGamepad(ucid) && !DebugProperties.FORCE_JOYSTICK;
controllerLogger.debugLog("Controller is gamepad: {}", isGamepad);
List<Driver> drivers = new ArrayList<>();
if ((SteamDeckUtil.DECK_MODE.isGamingMode() || DebugProperties.STEAM_DECK_CUSTOM_CEF_URL != null)
&& !steamDeckConsumed
&& hidInfo.type().namespace().equals(SteamDeckUtil.STEAM_DECK_NAMESPACE)
) {
controllerLogger.debugLog("Controller is steam deck candidate");
Optional<SteamDeckDriver> steamDeckDriver = SteamDeckDriver.create(controllerLogger);
if (steamDeckDriver.isPresent()) {
drivers.add(steamDeckDriver.get());
steamDeckConsumed = true;
controllerLogger.debugLog("Adding SteamDeckDriver - this controller has been reserved for Steam Deck");
}
}
if (isGamepad) {
SDL_Gamepad ptrGamepad = SDLUtil.openGamepad(jid);
drivers.add(new SDL3GamepadDriver(ptrGamepad, jid, hidInfo.type(), controllerLogger));
} else {
SDL_Joystick ptrJoystick = SDLUtil.openJoystick(jid);
drivers.add(new SDL3JoystickDriver(ptrJoystick, jid, hidInfo.type(), controllerLogger));
}
controllerLogger.debugLog("Drivers: {}", drivers.stream().map(driver -> driver.getClass().getSimpleName()).collect(Collectors.joining(", ")));
CompoundDriver compoundDriver = new CompoundDriver(drivers);
ControllerInfo info = new ControllerInfo(ucid, hidInfo.type(), hidInfo.hidDevice());
ControllerEntity controller = new ControllerEntity(
info,
compoundDriver,
this.controlify.config().getSettings().getOrCreateProfileSettings(info.type().namespace()),
ProfileSettings.createDefault(info.type().namespace()),
controllerLogger
);
controllerLogger.debugLog("Unique Controller ID: {}", info.ucid());
this.addController(ucid, controller);
return Optional.of(controller);
}
@Override
public boolean probeConnectedControllers() {
return SDL_HasJoystick() || SDL_HasGamepad();
}
@Override
public boolean isControllerGamepad(UniqueControllerID ucid) {
SDL_JoystickID jid = ((SDLUniqueControllerID) ucid).jid;
return SDL_IsGamepad(jid);
}
@Override
protected String getControllerSystemName(UniqueControllerID ucid) {
SDL_JoystickID jid = ((SDLUniqueControllerID) ucid).jid;
return isControllerGamepad(ucid) ? SDL_GetGamepadNameForID(jid) : SDL_GetJoystickNameForID(jid);
}
private Optional<ControllerEntity> getController(UniqueControllerID ucid) {
return Optional.ofNullable(controllersByJid.getOrDefault(ucid, null));
}
@Override
protected void loadGamepadMappings(ResourceProvider resourceProvider) {
CUtil.LOGGER.debugLog("Loading gamepad mappings...");
Optional<Resource> resourceOpt = resourceProvider
.getResource(CUtil.rl("controllers/gamecontrollerdb-sdl3.txt"));
if (resourceOpt.isEmpty()) {
CUtil.LOGGER.error("Failed to find game controller database.");
return;
}
try (InputStream is = resourceOpt.get().open()) {
byte[] bytes = ByteStreams.toByteArray(is);
try (Memory memory = new Memory(bytes.length)) {
memory.write(0, bytes, 0, bytes.length);
SDL_IOStream stream = SDL_IOFromConstMem(memory, new size_t(bytes.length));
if (stream == null) throw new IllegalStateException("Failed to open stream");
int count = SDL_AddGamepadMappingsFromIO(stream, true);
if (count < 0) {
CUtil.LOGGER.error("Failed to load gamepad mappings: {}", SDL_GetError());
} else if (count == 0) {
CUtil.LOGGER.warn("Successfully applied gamepad mappings but none were found for this OS. Unsupported OS?");
} else {
CUtil.LOGGER.log("Successfully loaded {} gamepad mapping entries!", count);
}
}
} catch (Throwable e) {
CUtil.LOGGER.error("Failed to load gamepad mappings", e);
}
}
private static Optional<ControllerHIDService.ControllerHIDInfo> fetchTypeFromSDL(SDL_JoystickID jid) {
int vid = SDL_GetJoystickVendorForID(jid);
int pid = SDL_GetJoystickProductForID(jid);
SDL_JoystickGUID guid = SDL_GetJoystickGUIDForID(jid);
String guidStr = guid.toString();
if (vid != 0 && pid != 0) {
CUtil.LOGGER.log("Using SDL to identify controller type.");
return Optional.of(new ControllerHIDService.ControllerHIDInfo(
Controlify.instance().controllerTypeManager().getControllerType(new HIDID(vid, pid)),
Optional.of(new HIDDevice.SDLHidApi(vid, pid, guidStr))
));
}
return Optional.empty();
}
public record SDLUniqueControllerID(@NotNull SDL_JoystickID jid) implements UniqueControllerID {
@Override
public boolean equals(Object obj) {
return obj instanceof SDLUniqueControllerID && ((SDLUniqueControllerID) obj).jid.equals(jid);
}
@Override
public String toString() {
return "SDL-" + jid.longValue();
}
@Override
public int hashCode() {
return Objects.hash(jid.longValue());
}
}
private static class EventFilter implements SDL_EventFilter {
@Override
public boolean filterEvent(Pointer userdata, SDL_Event event) {
switch (event.type) {
case SDL_EVENT_JOYSTICK_ADDED:
case SDL_EVENT_JOYSTICK_REMOVED:
return true;
default:
return false;
}
}
}
}