1
0
forked from Clones/Controlify

Finish & fix abstract controller manager + optimize hashmaps in codebase + no SDL screen

This commit is contained in:
Xander
2023-11-05 18:23:28 +00:00
parent b07066e097
commit f527f21fd2
24 changed files with 271 additions and 133 deletions

View File

@ -11,7 +11,7 @@ plugins {
}
group = "dev.isxander"
version = "1.7.0-beta.2+1.20"
version = "1.7.0-beta.3+1.20.2"
val isAlpha = "alpha" in version.toString()
val isBeta = "beta" in version.toString()
if (isAlpha) println("Controlify alpha version detected.")

View File

@ -0,0 +1,29 @@
# Controlify 1.7.0 (Beta 3) for 1.20.2
## New Features
- Added D-Pad snapping in container screens
- Keyboard-like movement whitelist and warning toast when joining new servers
- Added bind to open F3 debug screen
- More snap points on recipe book
- Allow users to define a custom SDL natives path (so you can put them in a common dir if you want)
- Add a reset all binds button to controls tab
## Changes
- Internal changes to the way controllers are discovered, loaded and managed. (this could introduce new bugs)
- `delegate_setup` config option has been renamed to `quiet_mode`.
- Pause screen's disconnect shortcut now focuses the button instead of clicking it.
- Add a donate button to the controller carousel screen.
- Modify how analogue inputs are processed whilst ingame or using the virtual mouse to make it feel more "circular"
- Marginally improve performance of Controlify by using optimized hashmaps.
## Bug Fixes
- Fix hotplugging when using natives.
- Fix SDL download screen progress bar being a missing texture.
- Fix people being unable to write newlines and spaces in signs when using mixed input mode.
- Fix some modded GUIs crashing when attempting to open when Controlify is loaded.
- Fix tridents not causing a vibration.
- Fix rumble not working on joysticks.
- Fix fabric mod json requirement allowing any 1.20 version not 1.20.2 and above.

View File

@ -3,6 +3,7 @@ package dev.isxander.controlify;
import com.mojang.blaze3d.Blaze3D;
import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.compatibility.ControlifyCompat;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
@ -63,10 +64,12 @@ public class Controlify implements ControlifyApi {
private boolean probeMode = false;
private Controller<?, ?> currentController = null;
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
private InGameInputHandler inGameInputHandler;
public InGameButtonGuide inGameButtonGuide;
private VirtualMouseHandler virtualMouseHandler;
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
private ControllerHIDService controllerHIDService;
private CompletableFuture<Boolean> nativeOnboardingFuture = null;
@ -81,10 +84,6 @@ public class Controlify implements ControlifyApi {
private int showMouseTicks = 0;
private @Nullable Controller<?, ?> switchableController = null;
private double askSwitchTime = 0;
private ToastUtils.ControlifyToast askSwitchToast = null;
/**
* Called at usual fabric client entrypoint.
* Always runs, even with no controllers detected.
@ -178,10 +177,6 @@ public class Controlify implements ControlifyApi {
ClientTickEvents.END_CLIENT_TICK.register(client -> this.probeTick());
}
// initialise and compatability modules that controlify implements itself
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
ControlifyCompat.init();
// register events
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
controllerHIDService().stop();
@ -211,19 +206,28 @@ public class Controlify implements ControlifyApi {
Log.LOGGER.info("No controllers found.");
}
// if no controller is currently selected, select the first one
// if no controller is currently selected, pick one
if (getCurrentController().isEmpty()) {
Controller<?, ?> controller = controllerManager.getConnectedControllers().stream().findFirst().orElse(null);
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
controller = null;
}
Optional<Controller<?, ?>> lastUsedController = controllerManager.getConnectedControllers()
.stream()
.filter(c -> c.uid().equals(config().currentControllerUid()))
.findAny();
this.setCurrentController(controller, false);
} else {
// setCurrentController saves config so there is no need to set dirty to save
config().saveIfDirty();
if (lastUsedController.isPresent()) {
this.setCurrentController(lastUsedController.get(), false);
} else {
Controller<?, ?> anyController = controllerManager.getConnectedControllers()
.stream()
.filter(c -> !c.config().delayedCalibration && c.config().deadzonesCalibrated)
.findFirst()
.orElse(null);
this.setCurrentController(anyController, false);
}
}
config().saveIfDirty();
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
try {
entrypoint.onControllersDiscovered(this);
@ -233,10 +237,16 @@ public class Controlify implements ControlifyApi {
});
}
/**
* Completely finishes controlify initialization.
* This can be run at any point during the game's lifecycle.
* @return the future that completes when controlify has finished initializing
*/
public CompletableFuture<Void> finishControlifyInit() {
if (finishedInit) {
return CompletableFuture.completedFuture(null);
}
probeMode = false;
finishedInit = true;
askNatives().whenComplete((loaded, th) -> {
@ -248,12 +258,33 @@ public class Controlify implements ControlifyApi {
ConnectServerEvent.EVENT.register((minecraft, address, data) -> {
notifyNewServer(data);
});
// initialise and compatability modules that controlify implements itself
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
ControlifyCompat.init();
// make sure people don't someone add binds after controllers could have been created
ControllerBindings.lockRegistry();
if (config().globalSettings().quietMode) {
config().globalSettings().quietMode = false;
config().setDirty();
}
discoverControllers();
});
return askNatives().thenApply(loaded -> null);
}
/**
* Called when a controller is connected. Either from controller
* discovery or hotplugging.
*
* @param controller the new controller
* @param hotplugged if this was a result of hotplugging
* @param newController if this controller has never been seen before
*/
private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged, boolean newController) {
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
@ -264,12 +295,10 @@ public class Controlify implements ControlifyApi {
config().setDirty();
}
if (hotplugged) {
if (controller.config().deadzonesCalibrated) {
setCurrentController(controller, hotplugged);
} else {
calibrationQueue.add(controller);
}
if (!controller.config().deadzonesCalibrated) {
calibrationQueue.add(controller);
} else if (hotplugged) {
setCurrentController(controller, true);
}
if (controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping) {
@ -278,7 +307,7 @@ public class Controlify implements ControlifyApi {
Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()),
true
);
} else {
} else if (hotplugged) {
ToastUtils.sendToast(
Component.translatable("controlify.toast.controller_connected.title"),
Component.translatable("controlify.toast.controller_connected.description", controller.name()),
@ -289,8 +318,17 @@ public class Controlify implements ControlifyApi {
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
controllerListScreen.refreshControllers();
}
// saved after discovery
if (hotplugged) {
config().saveIfDirty();
}
}
/**
* Called when a controller is disconnected.
* @param controller controller that has been disconnected
*/
private void onControllerRemoved(Controller<?, ?> controller) {
this.setCurrentController(
controllerManager.getConnectedControllers()
@ -325,6 +363,14 @@ public class Controlify implements ControlifyApi {
if (nativeOnboardingFuture != null)
return nativeOnboardingFuture;
// just say no if the platform doesn't support it
if (!SDL2NativesManager.isSupportedOnThisPlatform()) {
Log.LOGGER.warn("SDL is not supported on this platform. Platform: {}", SDL2NativesManager.Target.CURRENT);
nativeOnboardingFuture = new CompletableFuture<>();
minecraft.setScreen(new NoSDLScreen(() -> nativeOnboardingFuture.complete(false), minecraft.screen));
return nativeOnboardingFuture;
}
// the user has already been asked, initialise SDL if necessary
// and return a completed future
if (config().globalSettings().vibrationOnboarded) {
@ -373,19 +419,10 @@ public class Controlify implements ControlifyApi {
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
// handles updating state of all controllers
controllerManager.tick(outOfFocus);
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
if (switchableController.state().hasAnyInput()) {
switchableController.clearState();
this.setCurrentController(switchableController, true); // setCurrentController sets switchableController to null
if (askSwitchToast != null) {
askSwitchToast.remove();
askSwitchToast = null;
}
}
}
// handle showing/hiding mouse whilst in mixed input mode
if (minecraft.mouseHandler.isMouseGrabbed())
showMouseTicks = 0;
if (currentInputMode() == InputMode.MIXED && showMouseTicks > 0) {
@ -400,6 +437,7 @@ public class Controlify implements ControlifyApi {
LowBatteryNotifier.tick();
// if splitscreen ever happens this can tick over every controller
getCurrentController().ifPresent(currentController -> {
wrapControllerError(
() -> tickController(currentController, outOfFocus),
@ -409,6 +447,12 @@ public class Controlify implements ControlifyApi {
});
}
/**
* Ticks a specific controller.
*
* @param controller controller to tick
* @param outOfFocus if the window is out of focus
*/
private void tickController(Controller<?, ?> controller, boolean outOfFocus) {
ControllerState state = controller.state();
@ -479,10 +523,6 @@ public class Controlify implements ControlifyApi {
this.currentController = controller;
if (switchableController == controller) {
switchableController = null;
}
if (controller == null) {
this.setInputMode(InputMode.KEYBOARD_MOUSE);
this.inGameInputHandler = null;
@ -495,7 +535,7 @@ public class Controlify implements ControlifyApi {
DebugLog.log("Updated current controller to {}({})", controller.name(), controller.uid());
if (!controller.uid().equals(config().currentControllerUid())) {
config().save();
config().setDirty();
}
this.inGameInputHandler = new InGameInputHandler(controller);
@ -504,6 +544,8 @@ public class Controlify implements ControlifyApi {
setInputMode(InputMode.MIXED);
else if (changeInputMode)
setInputMode(InputMode.CONTROLLER);
config().saveIfDirty();
}
public Optional<ControllerManager> getControllerManager() {

View File

@ -3,7 +3,6 @@ package dev.isxander.controlify.api.bind;
import com.google.gson.JsonObject;
import dev.isxander.controlify.bindings.BindContext;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.bindings.RadialIcons;
import dev.isxander.yacl3.api.Option;
import net.minecraft.client.KeyMapping;
import net.minecraft.network.chat.Component;

View File

@ -17,6 +17,8 @@ import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize;
import dev.isxander.yacl3.api.Option;
import dev.isxander.yacl3.api.OptionDescription;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.locale.Language;
@ -39,9 +41,9 @@ public class ControllerBindingImpl<T extends ControllerState> implements Control
private final ResourceLocation radialIcon;
private final KeyMappingOverride override;
private static final Map<Controller<?, ?>, Set<IBind<?>>> pressedBinds = new HashMap<>();
private static final Map<Controller<?, ?>, Set<IBind<?>>> pressedBinds = new Object2ObjectOpenHashMap<>();
private int fakePressState = 0;
private byte fakePressState = 0;
private ControllerBindingImpl(Controller<T, ?> controller, IBind<T> defaultBind, ResourceLocation id, KeyMappingOverride vanillaOverride, Component name, Component description, Component category, Set<BindContext> contexts, ResourceLocation icon) {
this.controller = controller;
@ -219,7 +221,7 @@ public class ControllerBindingImpl<T extends ControllerState> implements Control
}
private static void addPressedBind(ControllerBindingImpl<?> binding) {
pressedBinds.computeIfAbsent(binding.controller, c -> new HashSet<>()).addAll(getBinds(binding.bind));
pressedBinds.computeIfAbsent(binding.controller, c -> new ObjectOpenHashSet<>()).addAll(getBinds(binding.bind));
}
private static Set<IBind<?>> getBinds(IBind<?> bind) {

View File

@ -14,6 +14,7 @@ import dev.isxander.controlify.mixins.compat.fapi.KeyBindingRegistryImplAccessor
import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor;
import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor;
import dev.isxander.controlify.utils.Log;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.ChatFormatting;
import net.minecraft.client.KeyMapping;
@ -23,6 +24,7 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.item.Items;
import org.apache.commons.lang3.Validate;
import java.util.*;
import java.util.function.BooleanSupplier;
@ -32,6 +34,7 @@ import java.util.function.UnaryOperator;
public class ControllerBindings<T extends ControllerState> {
private static final Map<ResourceLocation, Function<ControllerBindings<?>, ControllerBinding>> CUSTOM_BINDS = new LinkedHashMap<>();
private static final Set<KeyMapping> EXCLUDED_VANILLA_BINDS = new HashSet<>();
private static boolean lockRegistry = false;
public static final Component MOVEMENT_CATEGORY = Component.translatable("key.categories.movement");
public static final Component GAMEPLAY_CATEGORY = Component.translatable("key.categories.gameplay");
@ -75,7 +78,7 @@ public class ControllerBindings<T extends ControllerState> {
GUI_NAVI_UP, GUI_NAVI_DOWN, GUI_NAVI_LEFT, GUI_NAVI_RIGHT,
CYCLE_OPT_FORWARD, CYCLE_OPT_BACKWARD;
private final Map<ResourceLocation, ControllerBinding> registry = new LinkedHashMap<>();
private final Map<ResourceLocation, ControllerBinding> registry = new Object2ObjectLinkedOpenHashMap<>();
private final Controller<T, ?> controller;
@ -643,11 +646,18 @@ public class ControllerBindings<T extends ControllerState> {
}
}
public static void lockRegistry() {
Api.INSTANCE.lockRegistry();
}
public static final class Api implements ControlifyBindingsApi {
public static final Api INSTANCE = new Api();
private boolean lockedRegistry = false;
@Override
public dev.isxander.controlify.api.bind.BindingSupplier registerBind(ResourceLocation id, UnaryOperator<ControllerBindingBuilder<?>> builder) {
checkLocked();
CUSTOM_BINDS.put(id, bindings -> bindings.create(b -> builder.apply(b).identifier(id)));
return controller -> controller.bindings().get(id);
}
@ -655,6 +665,7 @@ public class ControllerBindings<T extends ControllerState> {
@Deprecated
@Override
public dev.isxander.controlify.api.bind.BindingSupplier registerBind(GamepadBinds bind, ResourceLocation id) {
checkLocked();
CUSTOM_BINDS.put(id, bindings -> bindings.create(bind, id));
return controller -> controller.bindings().get(id);
}
@ -662,18 +673,29 @@ public class ControllerBindings<T extends ControllerState> {
@Deprecated
@Override
public dev.isxander.controlify.api.bind.BindingSupplier registerBind(GamepadBinds bind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride) {
checkLocked();
CUSTOM_BINDS.put(id, bindings -> bindings.create(bind, id, override, toggleOverride));
return controller -> controller.bindings().get(id);
}
@Override
public void excludeVanillaBind(KeyMapping... keyMappings) {
checkLocked();
EXCLUDED_VANILLA_BINDS.addAll(Arrays.asList(keyMappings));
}
@Override
public void registerRadialIcon(ResourceLocation id, RadialIcon icon) {
checkLocked();
RadialIcons.registerIcon(id, icon);
}
public void lockRegistry() {
this.lockedRegistry = true;
}
private void checkLocked() {
Validate.isTrue(!lockedRegistry, "Cannot register new binds after registry is locked! You most likely tried to register a binding too late in Controlify's lifecycle.");
}
}
}

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.bindings;
import dev.isxander.controlify.api.bind.RadialIcon;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
@ -12,7 +13,6 @@ import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import java.util.HashMap;
import java.util.Map;
public final class RadialIcons {
@ -22,7 +22,7 @@ public final class RadialIcons {
public static final ResourceLocation FABRIC_ICON = new ResourceLocation("fabric-resource-loader-v0", "icon.png");
private static final Map<ResourceLocation, RadialIcon> icons = Util.make(() -> {
Map<ResourceLocation, RadialIcon> map = new HashMap<>();
Map<ResourceLocation, RadialIcon> map = new Object2ObjectOpenHashMap<>();
map.put(EMPTY, (graphics, x, y, tickDelta) -> {});
map.put(FABRIC_ICON, (graphics, x, y, tickDelta) -> {

View File

@ -63,8 +63,8 @@ public class ControlifyConfig {
Log.LOGGER.info("Loading Controlify config...");
if (!Files.exists(CONFIG_PATH)) {
firstLaunch = true;
if (lastSeenVersion == null) {
firstLaunch = true;
try {
lastSeenVersion = Version.parse("0.0.0");
} catch (VersionParsingException e) {

View File

@ -29,11 +29,6 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
protected C config, defaultConfig;
public AbstractController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
if (joystickId > GLFW.GLFW_JOYSTICK_LAST || joystickId < 0)
throw new IllegalArgumentException("Joystick ID " + joystickId + " is out of range!");
if (!GLFW.glfwJoystickPresent(joystickId))
throw new IllegalArgumentException("Joystick " + joystickId + " is not present and cannot be initialised!");
this.hidInfo = hidInfo;
this.joystickId = joystickId;

View File

@ -2,13 +2,15 @@ package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerConfig;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap;
import org.apache.commons.lang3.Validate;
import java.util.*;
public class JoystickConfig extends ControllerConfig {
private Map<String, Float> deadzones;
private Set<Integer> triggerAxes = new HashSet<>();
private Set<Integer> triggerAxes = new IntOpenHashSet();
private transient JoystickController<?> controller;
@ -55,7 +57,7 @@ public class JoystickConfig extends ControllerConfig {
this.controller = controller;
if (this.deadzones == null) {
deadzones = new HashMap<>();
deadzones = new Object2FloatOpenHashMap<>();
for (int i = 0; i < controller.mapping().axes().length; i++) {
JoystickMapping.Axis axis = controller.mapping().axes()[i];

View File

@ -13,13 +13,13 @@ import dev.isxander.controlify.hid.HIDDevice;
import dev.isxander.controlify.utils.ControllerUtils;
import dev.isxander.controlify.utils.DebugLog;
import dev.isxander.controlify.utils.Log;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
import net.minecraft.client.Minecraft;
import net.minecraft.server.packs.resources.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -29,7 +29,8 @@ import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError;
public abstract class AbstractControllerManager implements ControllerManager {
protected final Controlify controlify;
protected final Minecraft minecraft;
private final Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
protected final Map<String, Controller<?, ?>> controllersByUid = new Object2ObjectOpenHashMap<>();
public AbstractControllerManager() {
this.controlify = Controlify.instance();
@ -40,37 +41,35 @@ public abstract class AbstractControllerManager implements ControllerManager {
.ifPresent(this::loadGamepadMappings);
}
public Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
public Optional<Controller<?, ?>> createOrGet(int joystickIndex, ControllerHIDService.ControllerHIDInfo hidInfo) {
try {
Optional<String> uid = hidInfo.createControllerUID();
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
return Optional.of(CONTROLLERS.get(uid.get()));
if (uid.isPresent() && controllersByUid.containsKey(uid.get())) {
return Optional.of(controllersByUid.get(uid.get()));
}
if (hidInfo.type().dontLoad()) {
DebugLog.log("Preventing load of controller #" + joystickId + " because its type prevents loading.");
DebugLog.log("Preventing load of controller #" + joystickIndex + " because its type prevents loading.");
return Optional.empty();
}
if (this.isControllerGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) {
GamepadController controller = new GamepadController(joystickId, hidInfo);
CONTROLLERS.put(controller.uid(), controller);
checkCompoundJoysticks();
if (this.isControllerGamepad(joystickIndex) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) {
GamepadController controller = new GamepadController(joystickIndex, hidInfo);
this.addController(joystickIndex, controller);
return Optional.of(controller);
}
SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
CONTROLLERS.put(controller.uid(), controller);
checkCompoundJoysticks();
SingleJoystickController controller = new SingleJoystickController(joystickIndex, hidInfo);
this.addController(joystickIndex, controller);
return Optional.of(controller);
} catch (Throwable e) {
CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickId);
CrashReport crashReport = CrashReport.forThrowable(e, "Creating controller #" + joystickIndex);
CrashReportCategory category = crashReport.addCategory("Controller Info");
category.setDetail("Joystick ID", joystickId);
category.setDetail("Joystick ID", joystickIndex);
category.setDetail("Controller identification", hidInfo.type());
category.setDetail("HID path", hidInfo.hidDevice().map(HIDDevice::path).orElse("N/A"));
category.setDetail("HID service status", Controlify.instance().controllerHIDService().isDisabled() ? "Disabled" : "Enabled");
category.setDetail("GLFW name", Optional.ofNullable(getControllerSystemName(joystickId)).orElse("N/A"));
category.setDetail("GLFW name", Optional.ofNullable(getControllerSystemName(joystickIndex)).orElse("N/A"));
throw new ReportedException(crashReport);
}
}
@ -86,11 +85,6 @@ public abstract class AbstractControllerManager implements ControllerManager {
}
}
@Override
public Optional<Controller<?, ?>> getController(int joystickId) {
return CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == joystickId).findAny();
}
protected void onControllerConnected(Controller<?, ?> controller, boolean hotplug) {
Log.LOGGER.info("Controller connected: {}", ControllerUtils.createControllerString(controller));
@ -103,20 +97,19 @@ public abstract class AbstractControllerManager implements ControllerManager {
Log.LOGGER.info("Controller disconnected: {}", ControllerUtils.createControllerString(controller));
controller.hidInfo().ifPresent(controlify.controllerHIDService()::unconsumeController);
removeController(controller);
removeController(controller.uid());
checkCompoundJoysticks();
ControlifyEvents.CONTROLLER_DISCONNECTED.invoker().onControllerDisconnected(controller);
}
protected void removeController(Controller<?, ?> controller) {
controller.close();
CONTROLLERS.remove(controller.uid(), controller);
protected void addController(int id, Controller<?, ?> controller) {
controllersByUid.put(controller.uid(), controller);
checkCompoundJoysticks();
}
protected void removeController(String uid) {
Controller<?, ?> prev = CONTROLLERS.remove(uid);
Controller<?, ?> prev = controllersByUid.remove(uid);
if (prev != null) {
prev.close();
}
@ -126,17 +119,17 @@ public abstract class AbstractControllerManager implements ControllerManager {
@Override
public List<Controller<?, ?>> getConnectedControllers() {
return ImmutableList.copyOf(CONTROLLERS.values());
return ImmutableList.copyOf(controllersByUid.values());
}
@Override
public boolean isControllerConnected(String uid) {
return CONTROLLERS.containsKey(uid);
return controllersByUid.containsKey(uid);
}
@Override
public void close() {
CONTROLLERS.values().forEach(Controller::close);
controllersByUid.values().forEach(Controller::close);
}
protected abstract void loadGamepadMappings(Resource resource);
@ -154,7 +147,7 @@ public abstract class AbstractControllerManager implements ControllerManager {
if (!info.isLoaded() && info.canBeUsed()) {
Log.LOGGER.info("Loading compound joystick " + info.type().mappingId() + ".");
CompoundJoystickController controller = info.attemptCreate().orElseThrow();
CONTROLLERS.put(info.type().mappingId(), controller);
controllersByUid.put(info.type().mappingId(), controller);
Controlify.instance().config().loadOrCreateControllerData(controller);
}
} catch (Exception e) {

View File

@ -14,8 +14,6 @@ public interface ControllerManager {
List<Controller<?, ?>> getConnectedControllers();
Optional<Controller<?, ?>> getController(int jid);
boolean isControllerConnected(String uid);
boolean isControllerGamepad(int jid);

View File

@ -58,11 +58,6 @@ public class GLFWControllerManager extends AbstractControllerManager {
return areControllersConnected();
}
@Override
public void tick(boolean outOfFocus) {
}
@Override
protected void loadGamepadMappings(Resource resource) {
Log.LOGGER.debug("Loading gamepad mappings...");
@ -79,6 +74,10 @@ public class GLFWControllerManager extends AbstractControllerManager {
}
}
private Optional<Controller<?, ?>> getController(int joystickId) {
return controllersByUid.values().stream().filter(controller -> controller.joystickId() == joystickId).findAny();
}
@Override
public boolean isControllerGamepad(int jid) {
return GLFW.glfwJoystickIsGamepad(jid);

View File

@ -5,11 +5,15 @@ import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.driver.SDL2NativesManager;
import dev.isxander.controlify.hid.ControllerHIDService;
import dev.isxander.controlify.utils.Log;
import io.github.libsdl4j.api.event.SDL_Event;
import io.github.libsdl4j.api.event.SDL_EventFilter;
import io.github.libsdl4j.api.rwops.SDL_RWops;
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.client.Minecraft;
import net.minecraft.server.packs.resources.Resource;
import org.apache.commons.lang3.Validate;
@ -17,17 +21,17 @@ import org.apache.commons.lang3.Validate;
import java.io.InputStream;
import java.util.Optional;
import static io.github.libsdl4j.api.error.SdlError.SDL_GetError;
import static io.github.libsdl4j.api.error.SdlError.*;
import static io.github.libsdl4j.api.event.SDL_EventType.*;
import static io.github.libsdl4j.api.event.SdlEvents.*;
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.*;
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
import static io.github.libsdl4j.api.rwops.SdlRWops.*;
public class SDLControllerManager extends AbstractControllerManager {
private final Controlify controlify;
private final Minecraft minecraft;
private final Int2ObjectMap<Controller<?, ?>> controllersByJid = new Int2ObjectArrayMap<>();
private final SDL_Event event = new SDL_Event();
// must keep a reference to prevent GC from collecting it and the callback failing
@ -38,34 +42,34 @@ public class SDLControllerManager extends AbstractControllerManager {
Validate.isTrue(SDL2NativesManager.isLoaded(), "SDL2 natives must be loaded before creating SDLControllerManager");
this.controlify = Controlify.instance();
this.minecraft = Minecraft.getInstance();
SDL_SetEventFilter(eventFilter = new EventFilter(), Pointer.NULL);
}
@Override
public void tick(boolean outOfFocus) {
super.tick(outOfFocus);
// SDL identifiers controllers in two different ways:
// device index, and device instance ID.
while (SDL_PollEvent(event) == 1) {
switch (event.type) {
// On added, `which` refers to the device index
case SDL_JOYDEVICEADDED -> {
int jid = event.jdevice.which;
Optional<Controller<?, ?>> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid));
controllerOpt.ifPresent(controller -> onControllerConnected(controller, true));
}
case SDL_CONTROLLERDEVICEADDED -> {
int jid = event.cdevice.which;
Optional<Controller<?, ?>> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid));
int deviceIndex = event.jdevice.which;
Optional<Controller<?, ?>> controllerOpt = createOrGet(
deviceIndex,
ControllerHIDService.fetchTypeFromSDL(deviceIndex)
.orElse(new ControllerHIDService.ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()))
);
controllerOpt.ifPresent(controller -> onControllerConnected(controller, true));
}
// On removed, `which` refers to the device instance ID
case SDL_JOYDEVICEREMOVED -> {
int jid = event.jdevice.which;
getController(jid).ifPresent(this::onControllerRemoved);
}
case SDL_CONTROLLERDEVICEREMOVED -> {
int jid = event.cdevice.which;
getController(jid).ifPresent(this::onControllerRemoved);
}
}
}
}
@ -93,6 +97,19 @@ public class SDLControllerManager extends AbstractControllerManager {
return isControllerGamepad(joystickId) ? SDL_GameControllerNameForIndex(joystickId) : SDL_JoystickNameForIndex(joystickId);
}
@Override
protected void addController(int index, Controller<?, ?> controller) {
super.addController(index, controller);
// the instance id is technically a long, but it's usually only like 0, 1, 2, 3, etc.
int joystickId = SDL_JoystickGetDeviceInstanceID(index).intValue();
controllersByJid.put(joystickId, controller);
}
private Optional<Controller<?, ?>> getController(int joystickId) {
return Optional.ofNullable(controllersByJid.get(joystickId));
}
@Override
protected void loadGamepadMappings(Resource resource) {
Log.LOGGER.debug("Loading gamepad mappings...");
@ -119,8 +136,6 @@ public class SDLControllerManager extends AbstractControllerManager {
switch (event.type) {
case SDL_JOYDEVICEADDED:
case SDL_JOYDEVICEREMOVED:
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
return 1;
default:
return 0;

View File

@ -23,7 +23,7 @@ public class DebugProperties {
/** Print what drivers are being used */
public static final boolean PRINT_DRIVER = boolProp("controlify.debug.print_driver", true, true);
/** Print the state of the left and right triggers on gamepads */
public static final boolean PRINT_GAMEPAD_STATE = boolProp("controlify.debug.print_gamepad_state", false, true);
public static final boolean PRINT_GAMEPAD_STATE = boolProp("controlify.debug.print_gamepad_state", false, false);
/** Use experimental anti-snapback */
public static final boolean USE_SNAPBACK = boolProp("controlify.debug.use_snapback", false, false);

View File

@ -54,6 +54,11 @@ public class SDL2NativesManager {
attemptedLoad = true;
if (!isSupportedOnThisPlatform()) {
Log.LOGGER.warn("No native library for current platform, skipping SDL2 load");
return initFuture = CompletableFuture.completedFuture(false);
}
Path localLibraryPath = getNativesFolderPath().resolve(Target.CURRENT.getArtifactName());
if (Files.exists(localLibraryPath)) {
@ -178,6 +183,10 @@ public class SDL2NativesManager {
return attemptedLoad;
}
public static boolean isSupportedOnThisPlatform() {
return Target.CURRENT.hasNativeLibrary();
}
private static Path getNativesFolderPath() {
Path nativesFolderPath = FabricLoader.getInstance().getGameDir();
ControlifyConfig config = Controlify.instance().config();

View File

@ -63,12 +63,12 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
// Triggers are in the range [0, 32767] (thanks SDL!)
// https://wiki.libsdl.org/SDL2/SDL_GameControllerGetAxis
GamepadState.AxesState axes = new GamepadState.AxesState(
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERLEFT), 0, Short.MAX_VALUE),
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT), 0, Short.MAX_VALUE)
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTX)),
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTY)),
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTX)),
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTY)),
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERLEFT)),
mapShortToFloat(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT))
);
// Button values return 1 if pressed, 0 if not
// https://wiki.libsdl.org/SDL2/SDL_GameControllerGetButton
@ -190,4 +190,9 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
public String getBasicGamepadDetails() {
return "SDL2gp";
}
private static float mapShortToFloat(short value) {
return Mth.clampedMap(value, Short.MIN_VALUE, 0, -1f, 0f)
+ Mth.clampedMap(value, 0, Short.MAX_VALUE, 0f, 1f);
}
}

View File

@ -201,7 +201,10 @@ public class InGameButtonGuide implements IngameGuideRegistry {
registerGuideAction(controller.bindings().ATTACK, ActionLocation.RIGHT, (ctx) -> {
var hitResult = ctx.hitResult();
if (hitResult.getType() == HitResult.Type.ENTITY)
return Optional.of(Component.translatable("controlify.guide.ingame.attack"));
if (player.isSpectator())
return Optional.of(Component.translatable("controlify.guide.ingame.spectate"));
else
return Optional.of(Component.translatable("controlify.guide.ingame.attack"));
if (hitResult.getType() == HitResult.Type.BLOCK)
return Optional.of(Component.translatable("controlify.guide.ingame.break"));
return Optional.empty();

View File

@ -0,0 +1,22 @@
package dev.isxander.controlify.gui.screen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.AlertScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
public class NoSDLScreen extends AlertScreen {
public NoSDLScreen(Runnable actionHandler, Screen parent) {
super(
() -> {
actionHandler.run();
Minecraft.getInstance().setScreen(parent);
},
Component.translatable("controlify.gui.no_sdl.title"),
Component.translatable("controlify.gui.no_sdl.message"),
CommonComponents.GUI_OK,
false
);
}
}

View File

@ -7,9 +7,9 @@ import dev.isxander.controlify.driver.SDL2NativesManager;
import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.utils.Log;
import dev.isxander.controlify.utils.ToastUtils;
import io.github.libsdl4j.api.joystick.SDL_JoystickGUID;
import net.minecraft.network.chat.Component;
import org.hid4java.*;
import org.lwjgl.glfw.GLFW;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
@ -154,17 +154,17 @@ public class ControllerHIDService {
}
}
private Optional<ControllerHIDInfo> fetchTypeFromSDL(int jid) {
public static Optional<ControllerHIDInfo> fetchTypeFromSDL(int jid) {
if (SDL2NativesManager.isLoaded()) {
int vid = SDL_JoystickGetDeviceVendor(jid);
int pid = SDL_JoystickGetDeviceProduct(jid);
String path = GLFW.glfwGetJoystickGUID(jid);
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(jid);
if (vid != 0 && pid != 0) {
Log.LOGGER.info("Using SDL to identify controller type.");
return Optional.of(new ControllerHIDInfo(
ControllerType.getTypeForHID(new HIDIdentifier(vid, pid)),
Optional.of(new HIDDevice.SDLHidApi(vid, pid, path))
Optional.of(new HIDDevice.SDLHidApi(vid, pid, guid.toString()))
));
}
}

View File

@ -2,12 +2,13 @@ package dev.isxander.controlify.rumble;
import com.google.gson.JsonObject;
import dev.isxander.controlify.utils.Log;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.minecraft.resources.ResourceLocation;
import java.util.*;
public record RumbleSource(ResourceLocation id) {
private static final Map<ResourceLocation, RumbleSource> SOURCES = new LinkedHashMap<>();
private static final Map<ResourceLocation, RumbleSource> SOURCES = new Object2ObjectLinkedOpenHashMap<>();
public static final RumbleSource
MASTER = register("master"),

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.screenop;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@ -13,8 +13,8 @@ public class Registry<T, U> {
private final Map<T, U> cache;
public Registry() {
this.registry = new HashMap<>();
this.cache = new HashMap<>();
this.registry = new Object2ObjectOpenHashMap<>();
this.cache = new Object2ObjectOpenHashMap<>();
}
/**

View File

@ -1,5 +1,6 @@
package dev.isxander.controlify.screenop;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.gui.screens.Screen;
import java.util.HashMap;
@ -8,7 +9,7 @@ import java.util.function.Function;
@SuppressWarnings("unchecked")
public final class ScreenProcessorFactory {
private static final Map<Class<? extends Screen>, Factory<?>> factories = new HashMap<>();
private static final Map<Class<? extends Screen>, Factory<?>> factories = new Object2ObjectOpenHashMap<>();
private ScreenProcessorFactory() {
}

View File

@ -147,6 +147,9 @@
"controlify.gui.controller_unavailable": "Controller unavailable and cannot be edited.",
"controlify.gui.no_sdl.title": "Controlify Native Library Incompatible",
"controlify.gui.no_sdl.message": "Controlify uses an extra native library to add more features for controller support. Your system does not support this library. You will be missing out on features such as:\n - controller vibration\n - enhanced controller identification\n - gyroscope controls",
"controlify.new_features.title": "Controlify updated to %s!",
"controlify.new_features.1.5.0": "Added a radial menu that can be configured to any action you want. You can find it in your controller settings.",
@ -175,10 +178,8 @@
"controlify.toast.vmouse_disabled.description": "Controlify virtual mouse is now disabled for this screen.",
"controlify.toast.vmouse_unavailable.title": "Virtual Mouse Unavailable",
"controlify.toast.vmouse_unavailable.description": "This screen is forcing a specific virtual mouse mode and you cannot change it.",
"controlify.toast.ask_to_switch.title": "Switch Controller?",
"controlify.toast.ask_to_switch.description": "A new controller named '%s' has just been connected. Press any button to switch to it.",
"controlify.toast.default_controller_connected.title": "Controller Connected",
"controlify.toast.default_controller_connected.description": "Your primary controller has been connected and automatically switched to.",
"controlify.toast.controller_connected.title": "Controller Connected",
"controlify.toast.controller_connected.description": "A controller named `%s` has been connected. It has automatically been switched to.",
"controlify.toast.controller_disconnected.title": "Controller Disconnected",
"controlify.toast.controller_disconnected.description": "'%s' was disconnected.",
"controlify.toast.faulty_input.title": "Controller disabled",