1
0
forked from Clones/Controlify

compound joysticks, button guide in screens, improve API

This commit is contained in:
isXander
2023-03-26 18:13:02 +01:00
parent de210df84f
commit 0d9321e3ba
55 changed files with 1188 additions and 287 deletions

View File

@ -3,8 +3,10 @@ package dev.isxander.controlify;
import com.mojang.blaze3d.Blaze3D; import com.mojang.blaze3d.Blaze3D;
import com.mojang.logging.LogUtils; import com.mojang.logging.LogUtils;
import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState; import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.config.ControlifyConfig; import dev.isxander.controlify.config.ControlifyConfig;
@ -16,8 +18,8 @@ import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.utils.ToastUtils;
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -72,6 +74,8 @@ public class Controlify implements ControlifyApi {
} }
} }
checkCompoundJoysticks();
if (Controller.CONTROLLERS.isEmpty()) { if (Controller.CONTROLLERS.isEmpty()) {
LOGGER.info("No controllers found."); LOGGER.info("No controllers found.");
} }
@ -82,19 +86,41 @@ public class Controlify implements ControlifyApi {
// listen for new controllers // listen for new controllers
GLFW.glfwSetJoystickCallback((jid, event) -> { GLFW.glfwSetJoystickCallback((jid, event) -> {
if (event == GLFW.GLFW_CONNECTED) { try {
this.onControllerHotplugged(jid); if (event == GLFW.GLFW_CONNECTED) {
} else if (event == GLFW.GLFW_DISCONNECTED) { this.onControllerHotplugged(jid);
this.onControllerDisconnect(jid); } else if (event == GLFW.GLFW_DISCONNECTED) {
this.onControllerDisconnect(jid);
}
} catch (Exception e) {
e.printStackTrace();
} }
}); });
ClientTickEvents.START_CLIENT_TICK.register(this::tick); ClientTickEvents.START_CLIENT_TICK.register(this::tick);
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
try {
entrypoint.onControllersDiscovered(this);
} catch (Exception e) {
LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
}
});
} }
public void initializeControlify() { public void initializeControlify() {
LOGGER.info("Pre-initializing Controlify...");
this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller
this.virtualMouseHandler = new VirtualMouseHandler(); this.virtualMouseHandler = new VirtualMouseHandler();
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
try {
entrypoint.onControlifyPreInit(this);
} catch (Exception e) {
LOGGER.error("Failed to run `onControlifyPreInit` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
}
});
} }
public void tick(Minecraft client) { public void tick(Minecraft client) {
@ -109,8 +135,13 @@ public class Controlify implements ControlifyApi {
} }
} }
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
for (var controller : Controller.CONTROLLERS.values()) { for (var controller : Controller.CONTROLLERS.values()) {
controller.updateState(); if (!outOfFocus)
controller.updateState();
else
controller.clearState();
} }
ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state(); ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state();
@ -127,7 +158,7 @@ public class Controlify implements ControlifyApi {
} }
} }
if (!config().globalSettings().outOfFocusInput && !client.isWindowActive()) if (outOfFocus)
state = ControllerState.EMPTY; state = ControllerState.EMPTY;
if (state.hasAnyInput()) if (state.hasAnyInput())
@ -171,11 +202,14 @@ public class Controlify implements ControlifyApi {
config().loadOrCreateControllerData(currentController); config().loadOrCreateControllerData(currentController);
this.askToSwitchController(controller); this.askToSwitchController(controller);
checkCompoundJoysticks();
} }
private void onControllerDisconnect(int jid) { private void onControllerDisconnect(int jid) {
var controller = Controller.CONTROLLERS.remove(jid); Controller.CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> {
if (controller != null) { Controller.CONTROLLERS.remove(controller.uid(), controller);
setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null)); setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
LOGGER.info("Controller disconnected: " + controller.name()); LOGGER.info("Controller disconnected: " + controller.name());
this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
@ -185,7 +219,29 @@ public class Controlify implements ControlifyApi {
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()), Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
false false
); );
} });
checkCompoundJoysticks();
}
private void checkCompoundJoysticks() {
config().getCompoundJoysticks().values().forEach(info -> {
try {
if (info.isLoaded() && !info.canBeUsed()) {
LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers.");
Controller.CONTROLLERS.remove(info.type().identifier());
}
if (!info.isLoaded() && info.canBeUsed()) {
LOGGER.info("Loading compound joystick " + info.type().identifier() + ".");
CompoundJoystickController controller = info.attemptCreate().orElseThrow();
Controller.CONTROLLERS.put(info.type().identifier(), controller);
config().loadOrCreateControllerData(controller);
}
} catch (Exception e) {
e.printStackTrace();
}
});
} }
private void askToSwitchController(Controller<?, ?> controller) { private void askToSwitchController(Controller<?, ?> controller) {

View File

@ -9,6 +9,11 @@ import org.jetbrains.annotations.NotNull;
/** /**
* Interface with Controlify in a manner where you don't need to worry about updates * Interface with Controlify in a manner where you don't need to worry about updates
* breaking! This is the recommended way to interact with Controlify. * breaking! This is the recommended way to interact with Controlify.
* <p>
* Alternatively, to use Controlify directly, you can use {@link Controlify#instance()}. Though
* beware, things may break at any time!
* <p>
* Anything that is asked for from this API is safe to use, even if it is not in the API package.
*/ */
public interface ControlifyApi { public interface ControlifyApi {
/** /**
@ -16,6 +21,9 @@ public interface ControlifyApi {
*/ */
@NotNull Controller<?, ?> currentController(); @NotNull Controller<?, ?> currentController();
/**
* Get the current input mode for the game.
*/
@NotNull InputMode currentInputMode(); @NotNull InputMode currentInputMode();
void setInputMode(@NotNull InputMode mode); void setInputMode(@NotNull InputMode mode);

View File

@ -1,5 +1,6 @@
package dev.isxander.controlify.api.bind; package dev.isxander.controlify.api.bind;
import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.bindings.BindingSupplier; import dev.isxander.controlify.bindings.BindingSupplier;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.bindings.GamepadBinds; import dev.isxander.controlify.bindings.GamepadBinds;
@ -8,12 +9,17 @@ import net.minecraft.resources.ResourceLocation;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
/**
* Handles registering new bindings for controllers.
* <p>
* Should be called within {@link dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint#onControlifyPreInit(ControlifyApi)}
*/
public interface ControlifyBindingsApi { public interface ControlifyBindingsApi {
/** /**
* Registers a custom binding for all available controllers. * Registers a custom binding for all available controllers.
* If the controller is not a gamepad, the binding with be empty by default. * If the controller is not a gamepad, the binding with be empty by default.
* *
* @param bind the default gamepad bind * @param bind the default gamepad bind - joysticks are unset by default
* @param id the identifier for the binding, the namespace should be your modid. * @param id the identifier for the binding, the namespace should be your modid.
* @return the binding supplier to fetch the binding for a specific controller. * @return the binding supplier to fetch the binding for a specific controller.
*/ */

View File

@ -0,0 +1,34 @@
package dev.isxander.controlify.api.buttonguide;
import dev.isxander.controlify.bindings.ControllerBinding;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.gui.ButtonGuideRenderer;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.client.gui.screens.Screen;
import java.util.function.Function;
/**
* Adds a guide to a button. This does not invoke the button press on binding trigger, only renders the guide.
* This should be called every time a button is initialised, like in {@link Screen#init()}
*/
public interface ButtonGuideApi {
/**
* Makes the button render the image of the binding specified.
* This does not invoke the button press on binding trigger, only renders the guide.
* Custom behaviour should be handled inside a {@link dev.isxander.controlify.screenop.ScreenProcessor} or {@link dev.isxander.controlify.screenop.ComponentProcessor}
*
* @param button button to render the guide for
* @param binding gets the binding to render
* @param position where the guide should be rendered relative to the button
* @param renderPredicate whether the guide should be rendered
*/
static <T extends AbstractButton> void addGuideToButton(
T button,
Function<ControllerBindings<?>, ControllerBinding<?>> binding,
ButtonRenderPosition position,
ButtonGuidePredicate<T> renderPredicate) {
ButtonGuideRenderer.registerBindingForButton(button, binding, position, renderPredicate);
}
}

View File

@ -0,0 +1,13 @@
package dev.isxander.controlify.api.buttonguide;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.screens.Screen;
@FunctionalInterface
public interface ButtonGuidePredicate<T extends AbstractButton> {
boolean shouldDisplay(T button);
ButtonGuidePredicate<AbstractButton> FOCUS_ONLY = AbstractWidget::isFocused;
ButtonGuidePredicate<AbstractButton> ALWAYS = btn -> true;
}

View File

@ -0,0 +1,19 @@
package dev.isxander.controlify.api.buttonguide;
/**
* Where the guide should be rendered relative to the button.
*/
public enum ButtonRenderPosition {
/**
* Renders outside the button the left.
*/
LEFT,
/**
* Renders outside the button the right.
*/
RIGHT,
/**
* Renders inside the button on the left of the button text.
*/
TEXT
}

View File

@ -1,25 +0,0 @@
package dev.isxander.controlify.api.buttonguide;
import dev.isxander.controlify.controller.Controller;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.chat.Component;
import net.minecraft.world.phys.HitResult;
import java.util.Optional;
/**
* Supplies the text to display for a guide action based on the current context.
* If return is empty, the action will not be displayed.
*/
@FunctionalInterface
public interface GuideActionNameSupplier {
Optional<Component> supply(
Minecraft client,
LocalPlayer player,
ClientLevel level,
HitResult hitResult,
Controller<?, ?> controller
);
}

View File

@ -0,0 +1,20 @@
package dev.isxander.controlify.api.entrypoint;
import dev.isxander.controlify.api.ControlifyApi;
public interface ControlifyEntrypoint {
/**
* Called once Controlify has been fully initialised. And all controllers have
* been discovered and loaded.
* Due to the nature of the resource-pack system, this is called
* very late in the game's lifecycle (once the resources have been reloaded).
*/
void onControllersDiscovered(ControlifyApi controlify);
/**
* Called once Controlify has initialised some systems but controllers
* have not yet been discovered and constructed. This is the ideal
* time to register events in preparation for controller discovery.
*/
void onControlifyPreInit(ControlifyApi controlify);
}

View File

@ -3,7 +3,7 @@ package dev.isxander.controlify.api.event;
import dev.isxander.controlify.InputMode; import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.api.buttonguide.ButtonGuideRegistry; import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry;
import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory; import net.fabricmc.fabric.api.event.EventFactory;
@ -29,9 +29,9 @@ public final class ControlifyEvents {
/** /**
* Triggers when the button guide entries are being populated, so you can add more of your own. * Triggers when the button guide entries are being populated, so you can add more of your own.
*/ */
public static final Event<ButtonGuideRegistryEvent> BUTTON_GUIDE_REGISTRY = EventFactory.createArrayBacked(ButtonGuideRegistryEvent.class, callbacks -> (bindings, registry) -> { public static final Event<IngameGuideRegistryEvent> INGAME_GUIDE_REGISTRY = EventFactory.createArrayBacked(IngameGuideRegistryEvent.class, callbacks -> (bindings, registry) -> {
for (ButtonGuideRegistryEvent callback : callbacks) { for (IngameGuideRegistryEvent callback : callbacks) {
callback.onRegisterButtonGuide(bindings, registry); callback.onRegisterIngameGuide(bindings, registry);
} }
}); });
@ -55,8 +55,8 @@ public final class ControlifyEvents {
} }
@FunctionalInterface @FunctionalInterface
public interface ButtonGuideRegistryEvent { public interface IngameGuideRegistryEvent {
void onRegisterButtonGuide(ControllerBindings<?> bindings, ButtonGuideRegistry registry); void onRegisterIngameGuide(ControllerBindings<?> bindings, IngameGuideRegistry registry);
} }
@FunctionalInterface @FunctionalInterface

View File

@ -1,4 +1,4 @@
package dev.isxander.controlify.api.buttonguide; package dev.isxander.controlify.api.ingameguide;
/** /**
* Whether the action should be on the left or right list. * Whether the action should be on the left or right list.

View File

@ -1,4 +1,4 @@
package dev.isxander.controlify.api.buttonguide; package dev.isxander.controlify.api.ingameguide;
/** /**
* Defines how the action is sorted in the list. All default Controlify actions are {@link #NORMAL}. * Defines how the action is sorted in the list. All default Controlify actions are {@link #NORMAL}.

View File

@ -0,0 +1,16 @@
package dev.isxander.controlify.api.ingameguide;
import net.minecraft.network.chat.Component;
import java.util.Optional;
/**
* Supplies the text to display for a guide action based on the current context.
* If return is empty, the action will not be displayed.
* <p>
* This is supplied once every tick.
*/
@FunctionalInterface
public interface GuideActionNameSupplier {
Optional<Component> supply(IngameGuideContext ctx);
}

View File

@ -0,0 +1,14 @@
package dev.isxander.controlify.api.ingameguide;
import dev.isxander.controlify.controller.Controller;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.phys.HitResult;
public record IngameGuideContext(Minecraft client,
LocalPlayer player,
ClientLevel level,
HitResult hitResult,
Controller<?, ?> controller) {
}

View File

@ -1,13 +1,13 @@
package dev.isxander.controlify.api.buttonguide; package dev.isxander.controlify.api.ingameguide;
import dev.isxander.controlify.bindings.ControllerBinding; import dev.isxander.controlify.bindings.ControllerBinding;
/** /**
* Allows you to register your own actions to the button guide. * Allows you to register your own actions to the button guide.
* This should be called through {@link dev.isxander.controlify.api.event.ControlifyEvents#BUTTON_GUIDE_REGISTRY} as * This should be called through {@link dev.isxander.controlify.api.event.ControlifyEvents#INGAME_GUIDE_REGISTRY} as
* these should be called every time the guide is initialised. * these should be called every time the guide is initialised.
*/ */
public interface ButtonGuideRegistry { public interface IngameGuideRegistry {
/** /**
* Registers a new action to the button guide. * Registers a new action to the button guide.
* *

View File

@ -4,6 +4,7 @@ import java.util.Set;
/** /**
* An interface to implement by gui components to define snap points for virtual mouse snapping. * An interface to implement by gui components to define snap points for virtual mouse snapping.
* Can also be implemented in a mixin to improve compatibility.
*/ */
public interface ISnapBehaviour { public interface ISnapBehaviour {
Set<SnapPoint> getSnapPoints(); Set<SnapPoint> getSnapPoints();

View File

@ -34,6 +34,7 @@ public class ControllerBindings<T extends ControllerState> {
OPEN_CHAT, OPEN_CHAT,
GUI_PRESS, GUI_BACK, GUI_PRESS, GUI_BACK,
GUI_NEXT_TAB, GUI_PREV_TAB, GUI_NEXT_TAB, GUI_PREV_TAB,
GUI_ABSTRACT_ACTION_1, GUI_ABSTRACT_ACTION_2,
PICK_BLOCK, PICK_BLOCK,
TOGGLE_HUD_VISIBILITY, TOGGLE_HUD_VISIBILITY,
SHOW_PLAYER_LIST, SHOW_PLAYER_LIST,
@ -77,6 +78,8 @@ public class ControllerBindings<T extends ControllerState> {
register(GUI_BACK = new ControllerBinding<>(controller, GamepadBinds.B_BUTTON, new ResourceLocation("controlify", "gui_back"))); register(GUI_BACK = new ControllerBinding<>(controller, GamepadBinds.B_BUTTON, new ResourceLocation("controlify", "gui_back")));
register(GUI_NEXT_TAB = new ControllerBinding<>(controller, GamepadBinds.RIGHT_BUMPER, new ResourceLocation("controlify", "gui_next_tab"))); register(GUI_NEXT_TAB = new ControllerBinding<>(controller, GamepadBinds.RIGHT_BUMPER, new ResourceLocation("controlify", "gui_next_tab")));
register(GUI_PREV_TAB = new ControllerBinding<>(controller, GamepadBinds.LEFT_BUMPER, new ResourceLocation("controlify", "gui_prev_tab"))); register(GUI_PREV_TAB = new ControllerBinding<>(controller, GamepadBinds.LEFT_BUMPER, new ResourceLocation("controlify", "gui_prev_tab")));
register(GUI_ABSTRACT_ACTION_1 = new ControllerBinding<>(controller, GamepadBinds.X_BUTTON, new ResourceLocation("controlify", "gui_abstract_action_1")));
register(GUI_ABSTRACT_ACTION_2 = new ControllerBinding<>(controller, GamepadBinds.Y_BUTTON, new ResourceLocation("controlify", "gui_abstract_action_2")));
register(PICK_BLOCK = new ControllerBinding<>(controller, GamepadBinds.DPAD_LEFT, new ResourceLocation("controlify", "pick_block"), options.keyPickItem, () -> false)); register(PICK_BLOCK = new ControllerBinding<>(controller, GamepadBinds.DPAD_LEFT, new ResourceLocation("controlify", "pick_block"), options.keyPickItem, () -> false));
register(TOGGLE_HUD_VISIBILITY = new ControllerBinding<>(controller, new EmptyBind<>(), new ResourceLocation("controlify", "toggle_hud_visibility"))); register(TOGGLE_HUD_VISIBILITY = new ControllerBinding<>(controller, new EmptyBind<>(), new ResourceLocation("controlify", "toggle_hud_visibility")));
register(SHOW_PLAYER_LIST = new ControllerBinding<>(controller, GamepadBinds.DPAD_RIGHT, new ResourceLocation("controlify", "show_player_list"), options.keyPlayerList, () -> false)); register(SHOW_PLAYER_LIST = new ControllerBinding<>(controller, GamepadBinds.DPAD_RIGHT, new ResourceLocation("controlify", "show_player_list"), options.keyPlayerList, () -> false));
@ -138,6 +141,11 @@ public class ControllerBindings<T extends ControllerState> {
public void fromJson(JsonObject json) { public void fromJson(JsonObject json) {
for (var binding : registry().values()) { for (var binding : registry().values()) {
if (!json.has(binding.id().toString())) {
Controlify.LOGGER.warn("Missing control: " + binding.id() + " in config file. Skipping!");
continue;
}
var bind = json.get(binding.id().toString()).getAsJsonObject(); var bind = json.get(binding.id().toString()).getAsJsonObject();
if (bind == null) { if (bind == null) {
Controlify.LOGGER.warn("Unknown control: " + binding.id() + " in config file. Skipping!"); Controlify.LOGGER.warn("Unknown control: " + binding.id() + " in config file. Skipping!");

View File

@ -4,7 +4,7 @@ import com.google.gson.JsonObject;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.controller.*; import dev.isxander.controlify.controller.*;
import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.gui.DrawSize; import dev.isxander.controlify.gui.DrawSize;
public interface IBind<S extends ControllerState> { public interface IBind<S extends ControllerState> {
@ -28,7 +28,7 @@ public interface IBind<S extends ControllerState> {
if (controller instanceof GamepadController gamepad && type.equals(GamepadBinds.BIND_ID)) { if (controller instanceof GamepadController gamepad && type.equals(GamepadBinds.BIND_ID)) {
return GamepadBinds.fromJson(json).map(bind -> (IBind<T>) bind.forGamepad(gamepad)).orElse(new EmptyBind<>()); return GamepadBinds.fromJson(json).map(bind -> (IBind<T>) bind.forGamepad(gamepad)).orElse(new EmptyBind<>());
} else if (controller instanceof JoystickController joystick) { } else if (controller instanceof SingleJoystickController joystick) {
return (IBind<T>) switch (type) { return (IBind<T>) switch (type) {
case JoystickButtonBind.BIND_ID -> JoystickButtonBind.fromJson(json, joystick); case JoystickButtonBind.BIND_ID -> JoystickButtonBind.fromJson(json, joystick);
case JoystickHatBind.BIND_ID -> JoystickHatBind.fromJson(json, joystick); case JoystickHatBind.BIND_ID -> JoystickHatBind.fromJson(json, joystick);

View File

@ -18,11 +18,11 @@ import java.util.Objects;
public class JoystickAxisBind implements IBind<JoystickState> { public class JoystickAxisBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_axis"; public static final String BIND_ID = "joystick_axis";
private final JoystickController joystick; private final JoystickController<?> joystick;
private final int axisIndex; private final int axisIndex;
private final AxisDirection direction; private final AxisDirection direction;
public JoystickAxisBind(JoystickController joystick, int axisIndex, AxisDirection direction) { public JoystickAxisBind(JoystickController<?> joystick, int axisIndex, AxisDirection direction) {
this.joystick = joystick; this.joystick = joystick;
this.axisIndex = axisIndex; this.axisIndex = axisIndex;
this.direction = direction; this.direction = direction;
@ -93,7 +93,7 @@ public class JoystickAxisBind implements IBind<JoystickState> {
return Objects.hash(axisIndex, direction); return Objects.hash(axisIndex, direction);
} }
public static JoystickAxisBind fromJson(JsonObject object, JoystickController joystick) { public static JoystickAxisBind fromJson(JsonObject object, JoystickController<?> joystick) {
var axisIndex = object.get("axis").getAsInt(); var axisIndex = object.get("axis").getAsInt();
var direction = AxisDirection.valueOf(object.get("direction").getAsString()); var direction = AxisDirection.valueOf(object.get("direction").getAsString());
return new JoystickAxisBind(joystick, axisIndex, direction); return new JoystickAxisBind(joystick, axisIndex, direction);

View File

@ -7,7 +7,6 @@ import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize; import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiComponent; import net.minecraft.client.gui.GuiComponent;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -16,10 +15,10 @@ import java.util.Objects;
public class JoystickButtonBind implements IBind<JoystickState> { public class JoystickButtonBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_button"; public static final String BIND_ID = "joystick_button";
private final JoystickController joystick; private final JoystickController<?> joystick;
private final int buttonIndex; private final int buttonIndex;
public JoystickButtonBind(JoystickController joystick, int buttonIndex) { public JoystickButtonBind(JoystickController<?> joystick, int buttonIndex) {
this.joystick = joystick; this.joystick = joystick;
this.buttonIndex = buttonIndex; this.buttonIndex = buttonIndex;
} }
@ -71,7 +70,7 @@ public class JoystickButtonBind implements IBind<JoystickState> {
return Objects.hash(buttonIndex, joystick.uid()); return Objects.hash(buttonIndex, joystick.uid());
} }
public static JoystickButtonBind fromJson(JsonObject object, JoystickController joystick) { public static JoystickButtonBind fromJson(JsonObject object, JoystickController<?> joystick) {
var buttonIndex = object.get("button").getAsInt(); var buttonIndex = object.get("button").getAsInt();
return new JoystickButtonBind(joystick, buttonIndex); return new JoystickButtonBind(joystick, buttonIndex);
} }

View File

@ -7,7 +7,6 @@ import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.DrawSize; import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiComponent; import net.minecraft.client.gui.GuiComponent;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -16,11 +15,11 @@ import java.util.Objects;
public class JoystickHatBind implements IBind<JoystickState> { public class JoystickHatBind implements IBind<JoystickState> {
public static final String BIND_ID = "joystick_hat"; public static final String BIND_ID = "joystick_hat";
private final JoystickController joystick; private final JoystickController<?> joystick;
private final int hatIndex; private final int hatIndex;
private final JoystickState.HatState hatState; private final JoystickState.HatState hatState;
public JoystickHatBind(JoystickController joystick, int hatIndex, JoystickState.HatState hatState) { public JoystickHatBind(JoystickController<?> joystick, int hatIndex, JoystickState.HatState hatState) {
this.joystick = joystick; this.joystick = joystick;
this.hatIndex = hatIndex; this.hatIndex = hatIndex;
this.hatState = hatState; this.hatState = hatState;
@ -84,7 +83,7 @@ public class JoystickHatBind implements IBind<JoystickState> {
return Objects.hash(hatIndex, hatState, joystick.uid()); return Objects.hash(hatIndex, hatState, joystick.uid());
} }
public static JoystickHatBind fromJson(JsonObject object, JoystickController joystick) { public static JoystickHatBind fromJson(JsonObject object, JoystickController<?> joystick) {
var hatIndex = object.get("hat").getAsInt(); var hatIndex = object.get("hat").getAsInt();
var hatState = JoystickState.HatState.valueOf(object.get("state").getAsString()); var hatState = JoystickState.HatState.valueOf(object.get("state").getAsString());
return new JoystickHatBind(joystick, hatIndex, hatState); return new JoystickHatBind(joystick, hatIndex, hatState);

View File

@ -3,12 +3,17 @@ package dev.isxander.controlify.config;
import com.google.gson.*; import com.google.gson.*;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ControlifyConfig { public class ControlifyConfig {
public static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("controlify.json"); public static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("controlify.json");
@ -24,6 +29,7 @@ public class ControlifyConfig {
private String currentControllerUid; private String currentControllerUid;
private JsonObject controllerData = new JsonObject(); private JsonObject controllerData = new JsonObject();
private Map<String, CompoundJoystickInfo> compoundJoysticks = Map.of();
private GlobalSettings globalSettings = new GlobalSettings(); private GlobalSettings globalSettings = new GlobalSettings();
private boolean firstLaunch; private boolean firstLaunch;
@ -69,9 +75,10 @@ public class ControlifyConfig {
} }
controllerData = newControllerData; controllerData = newControllerData;
config.add("controllers", controllerData);
config.add("global", GSON.toJsonTree(globalSettings));
config.addProperty("current_controller", currentControllerUid = controlify.currentController().uid()); config.addProperty("current_controller", currentControllerUid = controlify.currentController().uid());
config.add("controllers", controllerData);
config.add("compound_joysticks", GSON.toJsonTree(compoundJoysticks.values().toArray(new CompoundJoystickInfo[0])));
config.add("global", GSON.toJsonTree(globalSettings));
return config; return config;
} }
@ -97,6 +104,13 @@ public class ControlifyConfig {
} }
} }
this.compoundJoysticks = object
.getAsJsonArray("compound_joysticks")
.asList()
.stream()
.map(element -> GSON.fromJson(element, CompoundJoystickInfo.class))
.collect(Collectors.toMap(info -> info.type().identifier(), Function.identity()));
if (object.has("current_controller")) { if (object.has("current_controller")) {
currentControllerUid = object.get("current_controller").getAsString(); currentControllerUid = object.get("current_controller").getAsString();
} else { } else {
@ -128,6 +142,14 @@ public class ControlifyConfig {
} }
} }
public Optional<JsonObject> getLoadedControllerConfig(String uid) {
return Optional.ofNullable(controllerData.getAsJsonObject(uid));
}
public Map<String, CompoundJoystickInfo> getCompoundJoysticks() {
return compoundJoysticks;
}
public GlobalSettings globalSettings() { public GlobalSettings globalSettings() {
return globalSettings; return globalSettings;
} }

View File

@ -3,6 +3,7 @@ package dev.isxander.controlify.config.gui;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.bindings.*; import dev.isxander.controlify.bindings.*;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessor;
@ -18,9 +19,9 @@ import org.lwjgl.glfw.GLFW;
public class JoystickBindController implements Controller<IBind<JoystickState>> { public class JoystickBindController implements Controller<IBind<JoystickState>> {
private final Option<IBind<JoystickState>> option; private final Option<IBind<JoystickState>> option;
private final JoystickController controller; private final JoystickController<?> controller;
public JoystickBindController(Option<IBind<JoystickState>> option, JoystickController controller) { public JoystickBindController(Option<IBind<JoystickState>> option, JoystickController<?> controller) {
this.option = option; this.option = option;
this.controller = controller; this.controller = controller;
} }

View File

@ -9,6 +9,7 @@ import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme; import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.yacl.api.*; import dev.isxander.yacl.api.*;
@ -79,14 +80,18 @@ public class YACLHelper {
} }
private static ConfigCategory createControllerCategory(Controller<?, ?> controller) { private static ConfigCategory createControllerCategory(Controller<?, ?> controller) {
if (!controller.canBeUsed()) {
return PlaceholderCategory.createBuilder()
.name(Component.literal(controller.name()))
.tooltip(Component.translatable("controlify.gui.controller_unavailable"))
.screen((minecraft, yacl) -> yacl)
.build();
}
var category = ConfigCategory.createBuilder(); var category = ConfigCategory.createBuilder();
category.name(Component.literal(controller.name())); category.name(Component.literal(controller.name()));
if (!controller.canBeUsed()) {
category.tooltip(Component.translatable("controlify.gui.controller_unavailable"));
}
var config = controller.config(); var config = controller.config();
var def = controller.defaultConfig(); var def = controller.defaultConfig();
@ -126,9 +131,15 @@ public class YACLHelper {
.controller(BooleanController::new) .controller(BooleanController::new)
.build()) .build())
.option(Option.createBuilder(boolean.class) .option(Option.createBuilder(boolean.class)
.name(Component.translatable("controlify.gui.show_guide")) .name(Component.translatable("controlify.gui.show_ingame_guide"))
.tooltip(Component.translatable("controlify.gui.show_guide.tooltip")) .tooltip(Component.translatable("controlify.gui.show_ingame_guide.tooltip"))
.binding(def.showGuide, () -> config.showGuide, v -> config.showGuide = v) .binding(def.showIngameGuide, () -> config.showIngameGuide, v -> config.showIngameGuide = v)
.controller(TickBoxController::new)
.build())
.option(Option.createBuilder(boolean.class)
.name(Component.translatable("controlify.gui.show_screen_guide"))
.tooltip(Component.translatable("controlify.gui.show_screen_guide.tooltip"))
.binding(def.showScreenGuide, () -> config.showScreenGuide, v -> config.showScreenGuide = v)
.controller(TickBoxController::new) .controller(TickBoxController::new)
.build()) .build())
.option(Option.createBuilder(float.class) .option(Option.createBuilder(float.class)
@ -182,8 +193,8 @@ public class YACLHelper {
var gpCfgDef = gamepad.defaultConfig(); var gpCfgDef = gamepad.defaultConfig();
advancedGroup advancedGroup
.option(Option.createBuilder(float.class) .option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.left_stick_deadzone")) .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick")))
.tooltip(Component.translatable("controlify.gui.left_stick_deadzone.tooltip")) .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick")))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding( .binding(
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY), Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
@ -193,8 +204,8 @@ public class YACLHelper {
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build()) .build())
.option(Option.createBuilder(float.class) .option(Option.createBuilder(float.class)
.name(Component.translatable("controlify.gui.right_stick_deadzone")) .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick")))
.tooltip(Component.translatable("controlify.gui.right_stick_deadzone.tooltip")) .tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick")))
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.binding( .binding(
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY), Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
@ -203,7 +214,7 @@ public class YACLHelper {
) )
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100)))) .controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
.build()); .build());
} else if (controller instanceof JoystickController joystick) { } else if (controller instanceof SingleJoystickController joystick) {
Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount()) Collection<Integer> deadzoneAxes = IntStream.range(0, joystick.axisCount())
.filter(i -> joystick.mapping().axis(i).requiresDeadzone()) .filter(i -> joystick.mapping().axis(i).requiresDeadzone())
.boxed() .boxed()
@ -254,7 +265,7 @@ public class YACLHelper {
.tooltip(binding.description()) .tooltip(binding.description())
.build()); .build());
} }
} else if (controller instanceof JoystickController joystick) { } else if (controller instanceof JoystickController<?> joystick) {
for (var binding : joystick.bindings().registry().values()) { for (var binding : joystick.bindings().registry().values()) {
controlsGroup.option(Option.createBuilder((Class<IBind<JoystickState>>) (Class<?>) IBind.class) controlsGroup.option(Option.createBuilder((Class<IBind<JoystickState>>) (Class<?>) IBind.class)
.name(binding.name()) .name(binding.name())

View File

@ -65,8 +65,8 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
} }
@Override @Override
public String guid() { public int joystickId() {
return this.guid; return this.joystickId;
} }
@Override @Override

View File

@ -5,7 +5,7 @@ import com.google.gson.JsonElement;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.debug.DebugProperties;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
@ -14,7 +14,7 @@ import java.util.Map;
public interface Controller<S extends ControllerState, C extends ControllerConfig> { public interface Controller<S extends ControllerState, C extends ControllerConfig> {
String uid(); String uid();
String guid(); int joystickId();
ControllerBindings<S> bindings(); ControllerBindings<S> bindings();
@ -31,26 +31,27 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
String name(); String name();
void updateState(); void updateState();
void clearState();
default boolean canBeUsed() { default boolean canBeUsed() {
return true; return true;
} }
Map<Integer, Controller<?, ?>> CONTROLLERS = new HashMap<>(); Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) { static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
if (CONTROLLERS.containsKey(joystickId)) { if (CONTROLLERS.containsKey(hidInfo.createControllerUID())) {
return CONTROLLERS.get(joystickId); return CONTROLLERS.get(hidInfo.createControllerUID());
} }
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) { if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) {
GamepadController controller = new GamepadController(joystickId, hidInfo); GamepadController controller = new GamepadController(joystickId, hidInfo);
CONTROLLERS.put(joystickId, controller); CONTROLLERS.put(controller.uid(), controller);
return controller; return controller;
} }
JoystickController controller = new JoystickController(joystickId, hidInfo); SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
CONTROLLERS.put(joystickId, controller); CONTROLLERS.put(controller.uid(), controller);
return controller; return controller;
} }
@ -74,8 +75,8 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
} }
@Override @Override
public String guid() { public int joystickId() {
return "DUMMY"; return -1;
} }
@Override @Override
@ -127,5 +128,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
public void updateState() { public void updateState() {
} }
@Override
public void clearState() {
}
}; };
} }

View File

@ -16,7 +16,8 @@ public abstract class ControllerConfig {
public String customName = null; public String customName = null;
public boolean showGuide = true; public boolean showIngameGuide = true;
public boolean showScreenGuide = true;
public float chatKeyboardHeight = 0f; public float chatKeyboardHeight = 0f;

View File

@ -43,6 +43,11 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
state = new GamepadState(axesState, rawAxesState, buttonState); state = new GamepadState(axesState, rawAxesState, buttonState);
} }
@Override
public void clearState() {
state = GamepadState.EMPTY;
}
public void consumeButtonState() { public void consumeButtonState() {
this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY); this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY);
} }

View File

@ -36,7 +36,7 @@ public class ControllerHIDService implements HidServicesListener {
services.scan(); services.scan();
try { try {
// wait for scan to complete on separate thread // wait for scan to complete on separate thread
Thread.sleep(800); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -78,5 +78,8 @@ public class ControllerHIDService implements HidServicesListener {
} }
public record ControllerHIDInfo(ControllerType type, Optional<String> path) { public record ControllerHIDInfo(ControllerType type, Optional<String> path) {
public String createControllerUID() {
return UUID.nameUUIDFromBytes(path().get().getBytes()).toString();
}
} }
} }

View File

@ -0,0 +1,158 @@
package dev.isxander.controlify.controller.joystick;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.List;
public class CompoundJoystickController implements JoystickController<JoystickConfig> {
private final String uid;
private final List<Integer> joysticks;
private final int axisCount, buttonCount, hatCount;
private final ControllerType compoundType;
private final ControllerBindings<JoystickState> bindings;
private final JoystickMapping mapping;
private JoystickConfig config;
private final JoystickConfig defaultConfig;
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
public CompoundJoystickController(List<Integer> joystickIds, String uid, ControllerType compoundType) {
this.joysticks = ImmutableList.copyOf(joystickIds);
this.uid = uid;
this.compoundType = compoundType;
this.axisCount = joystickIds.stream().mapToInt(this::getAxisCountForJoystick).sum();
this.buttonCount = joystickIds.stream().mapToInt(this::getButtonCountForJoystick).sum();
this.hatCount = joystickIds.stream().mapToInt(this::getHatCountForJoystick).sum();
this.mapping = RPJoystickMapping.fromType(type());
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
this.bindings = new ControllerBindings<>(this);
}
@Override
public String uid() {
return this.uid;
}
@Override
public ControllerBindings<JoystickState> bindings() {
return this.bindings;
}
@Override
public JoystickState state() {
return this.state;
}
@Override
public JoystickState prevState() {
return this.prevState;
}
@Override
public void updateState() {
this.prevState = this.state;
var states = this.joysticks.stream().map(joystick -> JoystickState.fromJoystick(this, joystick)).toList();
this.state = JoystickState.merged(mapping(), states);
}
@Override
public void clearState() {
this.state = JoystickState.empty(this);
}
@Override
public JoystickConfig config() {
return this.config;
}
@Override
public JoystickConfig defaultConfig() {
return this.defaultConfig;
}
@Override
public void resetConfig() {
this.config = new JoystickConfig(this);
}
@Override
public void setConfig(Gson gson, JsonElement json) {
JoystickConfig newConfig = gson.fromJson(json, JoystickConfig.class);
if (newConfig != null) {
this.config = newConfig;
} else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig();
}
this.config.setup(this);
}
@Override
public ControllerType type() {
return this.compoundType;
}
@Override
public String name() {
return type().friendlyName();
}
@Override
public JoystickMapping mapping() {
return this.mapping;
}
@Override
public int axisCount() {
return this.axisCount;
}
@Override
public int buttonCount() {
return this.buttonCount;
}
@Override
public int hatCount() {
return this.hatCount;
}
@Override
public boolean canBeUsed() {
return JoystickController.super.canBeUsed()
&& joysticks.stream().allMatch(GLFW::glfwJoystickPresent);
}
@Override
public int joystickId() {
return -1;
}
private int getAxisCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickAxes(joystick).capacity();
}
private int getButtonCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickButtons(joystick).capacity();
}
private int getHatCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickHats(joystick).capacity();
}
}

View File

@ -0,0 +1,46 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerType;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public record CompoundJoystickInfo(Collection<String> joystickUids, String friendlyName) {
public ControllerType type() {
return new ControllerType(friendlyName, createUID(joystickUids));
}
public boolean canBeUsed() {
List<Controller<?, ?>> joysticks = Controller.CONTROLLERS.values().stream().filter(c -> joystickUids.contains(c.uid())).toList();
if (joysticks.size() != joystickUids().size()) {
return false; // not all controllers are connected
}
if (joysticks.stream().anyMatch(c -> !c.canBeUsed())) {
return false; // not all controllers can be used
}
return true;
}
public boolean isLoaded() {
return Controller.CONTROLLERS.containsKey(createUID(joystickUids));
}
public Optional<CompoundJoystickController> attemptCreate() {
if (!canBeUsed()) return Optional.empty();
List<Integer> joystickIDs = Controller.CONTROLLERS.values().stream()
.filter(c -> joystickUids.contains(c.uid()))
.map(Controller::joystickId)
.toList();
ControllerType type = type();
return Optional.of(new CompoundJoystickController(joystickIDs, type.identifier(), type));
}
public static String createUID(Collection<String> joystickUIDs) {
return "compound-" + String.join("_", joystickUIDs);
}
}

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.controller.joystick; package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerConfig; import dev.isxander.controlify.controller.ControllerConfig;
import org.apache.commons.lang3.Validate;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -8,9 +9,10 @@ import java.util.Map;
public class JoystickConfig extends ControllerConfig { public class JoystickConfig extends ControllerConfig {
private Map<String, Float> deadzones; private Map<String, Float> deadzones;
private transient JoystickController controller; private transient JoystickController<?> controller;
public JoystickConfig(JoystickController controller) { public JoystickConfig(JoystickController<?> controller) {
Validate.notNull(controller);
setup(controller); setup(controller);
} }
@ -30,7 +32,7 @@ public class JoystickConfig extends ControllerConfig {
return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f); return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f);
} }
void setup(JoystickController controller) { void setup(JoystickController<?> controller) {
this.controller = controller; this.controller = controller;
if (this.deadzones == null) { if (this.deadzones == null) {
deadzones = new HashMap<>(); deadzones = new HashMap<>();

View File

@ -1,74 +1,20 @@
package dev.isxander.controlify.controller.joystick; package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson; import dev.isxander.controlify.controller.Controller;
import com.google.gson.JsonElement; import dev.isxander.controlify.controller.joystick.JoystickConfig;
import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping; import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.Objects; public interface JoystickController<T extends JoystickConfig> extends Controller<JoystickState, T> {
JoystickMapping mapping();
public class JoystickController extends AbstractController<JoystickState, JoystickConfig> { int axisCount();
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY; int buttonCount();
private final int axisCount, buttonCount, hatCount; int hatCount();
private final JoystickMapping mapping;
public JoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type()));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
}
@Override @Override
public JoystickState state() { default boolean canBeUsed() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
state = JoystickState.fromJoystick(this, joystickId);
}
public JoystickMapping mapping() {
return mapping;
}
@Override
public boolean canBeUsed() {
return !(mapping() instanceof UnmappedJoystickMapping); return !(mapping() instanceof UnmappedJoystickMapping);
} }
public int axisCount() {
return axisCount;
}
public int buttonCount() {
return buttonCount;
}
public int hatCount() {
return hatCount;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
super.setConfig(gson, json);
this.config.setup(this);
}
} }

View File

@ -11,6 +11,7 @@ import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -58,7 +59,17 @@ public class JoystickState implements ControllerState {
|| hats().stream().anyMatch(hat -> hat != HatState.CENTERED); || hats().stream().anyMatch(hat -> hat != HatState.CENTERED);
} }
public static JoystickState fromJoystick(JoystickController joystick, int joystickId) { @Override
public String toString() {
return "JoystickState{" +
"axes=" + axes +
", rawAxes=" + rawAxes +
", buttons=" + buttons +
", hats=" + hats +
'}';
}
public static JoystickState fromJoystick(JoystickController<?> joystick, int joystickId) {
FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystickId); FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystickId);
List<Float> axes = new ArrayList<>(); List<Float> axes = new ArrayList<>();
List<Float> rawAxes = new ArrayList<>(); List<Float> rawAxes = new ArrayList<>();
@ -107,6 +118,40 @@ public class JoystickState implements ControllerState {
return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats); return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats);
} }
public static JoystickState empty(JoystickController<?> joystick) {
var axes = new ArrayList<Float>();
var buttons = new ArrayList<Boolean>();
var hats = new ArrayList<HatState>();
for (int i = 0; i < joystick.axisCount(); i++) {
axes.add(joystick.mapping().axis(i).restingValue());
}
for (int i = 0; i < joystick.buttonCount(); i++) {
buttons.add(false);
}
for (int i = 0; i < joystick.hatCount(); i++) {
hats.add(HatState.CENTERED);
}
return new JoystickState(joystick.mapping(), axes, axes, buttons, hats);
}
public static JoystickState merged(JoystickMapping mapping, Collection<JoystickState> states) {
var axes = new ArrayList<Float>();
var rawAxes = new ArrayList<Float>();
var buttons = new ArrayList<Boolean>();
var hats = new ArrayList<HatState>();
for (var state : states) {
axes.addAll(state.axes);
rawAxes.addAll(state.rawAxes);
buttons.addAll(state.buttons);
hats.addAll(state.hats);
}
return new JoystickState(mapping, axes, rawAxes, buttons, hats);
}
public enum HatState implements NameableEnum { public enum HatState implements NameableEnum {
CENTERED, CENTERED,
UP, UP,

View File

@ -0,0 +1,83 @@
package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
public class SingleJoystickController extends AbstractController<JoystickState, JoystickConfig> implements JoystickController<JoystickConfig> {
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final int axisCount, buttonCount, hatCount;
private final JoystickMapping mapping;
public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type()));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
}
@Override
public JoystickState state() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
state = JoystickState.fromJoystick(this, joystickId);
}
@Override
public void clearState() {
this.state = JoystickState.empty(this);
}
@Override
public JoystickMapping mapping() {
return mapping;
}
@Override
public int axisCount() {
return axisCount;
}
@Override
public int buttonCount() {
return buttonCount;
}
@Override
public int hatCount() {
return hatCount;
}
@Override
public int joystickId() {
return joystickId;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
super.setConfig(gson, json);
this.config.setup(this);
}
}

View File

@ -21,6 +21,8 @@ public interface JoystickMapping {
boolean isAxisResting(float value); boolean isAxisResting(float value);
float restingValue();
String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction); String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction);
} }

View File

@ -111,7 +111,7 @@ public class RPJoystickMapping implements JoystickMapping {
} }
} }
private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restState, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis { private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis {
@Override @Override
public float modifyAxis(float value) { public float modifyAxis(float value) {
if (inpRange() == null || outRange() == null) if (inpRange() == null || outRange() == null)
@ -122,7 +122,7 @@ public class RPJoystickMapping implements JoystickMapping {
@Override @Override
public boolean isAxisResting(float value) { public boolean isAxisResting(float value) {
return value == restState(); return value == restingValue();
} }
@Override @Override

View File

@ -45,7 +45,12 @@ public class UnmappedJoystickMapping implements JoystickMapping {
@Override @Override
public boolean isAxisResting(float value) { public boolean isAxisResting(float value) {
return value == 0; return value == restingValue();
}
@Override
public float restingValue() {
return 0;
} }
@Override @Override

View File

@ -0,0 +1,24 @@
package dev.isxander.controlify.gui;
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
import dev.isxander.controlify.bindings.ControllerBinding;
import dev.isxander.controlify.bindings.ControllerBindings;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.client.gui.screens.Screen;
import java.util.function.Function;
/**
* @see dev.isxander.controlify.mixins.feature.guide.screen.AbstractButtonMixin
*/
public interface ButtonGuideRenderer<T extends AbstractButton> {
void setButtonGuide(RenderData<T> renderData);
static <T extends AbstractButton> void registerBindingForButton(T button, Function<ControllerBindings<?>, ControllerBinding<?>> binding, ButtonRenderPosition position, ButtonGuidePredicate<T> renderPredicate) {
((ButtonGuideRenderer<T>) button).setButtonGuide(new RenderData<>(binding, position, renderPredicate));
}
record RenderData<T extends AbstractButton>(Function<ControllerBindings<?>, ControllerBinding<?>> binding, ButtonRenderPosition position, ButtonGuidePredicate<T> renderPredicate) {
}
}

View File

@ -1,7 +1,7 @@
package dev.isxander.controlify.ingame.guide; package dev.isxander.controlify.ingame.guide;
import dev.isxander.controlify.api.buttonguide.ActionLocation; import dev.isxander.controlify.api.ingameguide.ActionLocation;
import dev.isxander.controlify.api.buttonguide.ActionPriority; import dev.isxander.controlify.api.ingameguide.ActionPriority;
import dev.isxander.controlify.bindings.ControllerBinding; import dev.isxander.controlify.bindings.ControllerBinding;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

View File

@ -1,10 +1,7 @@
package dev.isxander.controlify.ingame.guide; package dev.isxander.controlify.ingame.guide;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.api.buttonguide.ActionLocation; import dev.isxander.controlify.api.ingameguide.*;
import dev.isxander.controlify.api.buttonguide.ActionPriority;
import dev.isxander.controlify.api.buttonguide.ButtonGuideRegistry;
import dev.isxander.controlify.api.buttonguide.GuideActionNameSupplier;
import dev.isxander.controlify.bindings.ControllerBinding; import dev.isxander.controlify.bindings.ControllerBinding;
import dev.isxander.controlify.compatibility.ControlifyCompat; import dev.isxander.controlify.compatibility.ControlifyCompat;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
@ -23,7 +20,7 @@ import net.minecraft.world.phys.*;
import java.util.*; import java.util.*;
public class InGameButtonGuide implements ButtonGuideRegistry { public class InGameButtonGuide implements IngameGuideRegistry {
private final Controller<?, ?> controller; private final Controller<?, ?> controller;
private final LocalPlayer player; private final LocalPlayer player;
private final Minecraft minecraft = Minecraft.getInstance(); private final Minecraft minecraft = Minecraft.getInstance();
@ -38,11 +35,11 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
this.player = localPlayer; this.player = localPlayer;
registerDefaultActions(); registerDefaultActions();
ControlifyEvents.BUTTON_GUIDE_REGISTRY.invoker().onRegisterButtonGuide(controller.bindings(), this); ControlifyEvents.INGAME_GUIDE_REGISTRY.invoker().onRegisterIngameGuide(controller.bindings(), this);
} }
public void renderHud(PoseStack poseStack, float tickDelta, int width, int height) { public void renderHud(PoseStack poseStack, float tickDelta, int width, int height) {
if (!controller.config().showGuide || minecraft.screen != null || minecraft.options.renderDebug) if (!controller.config().showIngameGuide || minecraft.screen != null || minecraft.options.renderDebug)
return; return;
ControlifyCompat.ifBeginHudBatching(); ControlifyCompat.ifBeginHudBatching();
@ -98,7 +95,7 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
leftGuides.clear(); leftGuides.clear();
rightGuides.clear(); rightGuides.clear();
if (!controller.config().showGuide || minecraft.screen != null) if (!controller.config().showIngameGuide || minecraft.screen != null)
return; return;
for (var actionPredicate : guidePredicates) { for (var actionPredicate : guidePredicates) {
@ -131,7 +128,8 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
private void registerDefaultActions() { private void registerDefaultActions() {
var options = Minecraft.getInstance().options; var options = Minecraft.getInstance().options;
registerGuideAction(controller.bindings().JUMP, ActionLocation.LEFT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().JUMP, ActionLocation.LEFT, (ctx) -> {
var player = ctx.player();
if (player.getAbilities().flying) if (player.getAbilities().flying)
return Optional.of(Component.translatable("controlify.guide.fly_up")); return Optional.of(Component.translatable("controlify.guide.fly_up"));
@ -149,14 +147,15 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().SNEAK, ActionLocation.LEFT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().SNEAK, ActionLocation.LEFT, (ctx) -> {
var player = ctx.player();
if (player.getVehicle() != null) if (player.getVehicle() != null)
return Optional.of(Component.translatable("controlify.guide.dismount")); return Optional.of(Component.translatable("controlify.guide.dismount"));
if (player.getAbilities().flying) if (player.getAbilities().flying)
return Optional.of(Component.translatable("controlify.guide.fly_down")); return Optional.of(Component.translatable("controlify.guide.fly_down"));
if (player.isInWater()) if (player.isInWater())
return Optional.of(Component.translatable("controlify.guide.swim_down")); return Optional.of(Component.translatable("controlify.guide.swim_down"));
if (controller.config().toggleSneak) { if (ctx.controller().config().toggleSneak) {
if (player.input.shiftKeyDown) if (player.input.shiftKeyDown)
return Optional.of(Component.translatable("controlify.guide.stop_sneaking")); return Optional.of(Component.translatable("controlify.guide.stop_sneaking"));
else else
@ -167,33 +166,37 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
} }
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().SPRINT, ActionLocation.LEFT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().SPRINT, ActionLocation.LEFT, (ctx) -> {
var player = ctx.player();
if (!options.keySprint.isDown()) { if (!options.keySprint.isDown()) {
if (!player.input.getMoveVector().equals(Vec2.ZERO)) { if (!player.input.getMoveVector().equals(Vec2.ZERO)) {
if (player.isUnderWater()) if (player.isUnderWater())
return Optional.of(Component.translatable("controlify.guide.start_swimming")); return Optional.of(Component.translatable("controlify.guide.start_swimming"));
return Optional.of(Component.translatable("controlify.guide.start_sprinting")); return Optional.of(Component.translatable("controlify.guide.start_sprinting"));
} }
} else if (controller.config().toggleSprint) { } else if (ctx.controller().config().toggleSprint) {
if (player.isUnderWater()) if (player.isUnderWater())
return Optional.of(Component.translatable("controlify.guide.stop_swimming")); return Optional.of(Component.translatable("controlify.guide.stop_swimming"));
return Optional.of(Component.translatable("controlify.guide.stop_sprinting")); return Optional.of(Component.translatable("controlify.guide.stop_sprinting"));
} }
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().INVENTORY, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().INVENTORY, ActionLocation.RIGHT, (ctx) -> {
if (client.screen == null) if (ctx.client().screen == null)
return Optional.of(Component.translatable("controlify.guide.inventory")); return Optional.of(Component.translatable("controlify.guide.inventory"));
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().ATTACK, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().ATTACK, ActionLocation.RIGHT, (ctx) -> {
var hitResult = ctx.hitResult();
if (hitResult.getType() == HitResult.Type.ENTITY) if (hitResult.getType() == HitResult.Type.ENTITY)
return Optional.of(Component.translatable("controlify.guide.attack")); return Optional.of(Component.translatable("controlify.guide.attack"));
if (hitResult.getType() == HitResult.Type.BLOCK) if (hitResult.getType() == HitResult.Type.BLOCK)
return Optional.of(Component.translatable("controlify.guide.break")); return Optional.of(Component.translatable("controlify.guide.break"));
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().USE, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().USE, ActionLocation.RIGHT, (ctx) -> {
var hitResult = ctx.hitResult();
var player = ctx.player();
if (hitResult.getType() == HitResult.Type.ENTITY) if (hitResult.getType() == HitResult.Type.ENTITY)
if (player.isSpectator()) if (player.isSpectator())
return Optional.of(Component.translatable("controlify.guide.spectate")); return Optional.of(Component.translatable("controlify.guide.spectate"));
@ -203,18 +206,20 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
return Optional.of(Component.translatable("controlify.guide.use")); return Optional.of(Component.translatable("controlify.guide.use"));
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().DROP, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().DROP, ActionLocation.RIGHT, (ctx) -> {
var player = ctx.player();
if (player.hasItemInSlot(EquipmentSlot.MAINHAND) || player.hasItemInSlot(EquipmentSlot.OFFHAND)) if (player.hasItemInSlot(EquipmentSlot.MAINHAND) || player.hasItemInSlot(EquipmentSlot.OFFHAND))
return Optional.of(Component.translatable("controlify.guide.drop")); return Optional.of(Component.translatable("controlify.guide.drop"));
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().SWAP_HANDS, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().SWAP_HANDS, ActionLocation.RIGHT, (ctx) -> {
var player = ctx.player();
if (player.hasItemInSlot(EquipmentSlot.MAINHAND) || player.hasItemInSlot(EquipmentSlot.OFFHAND)) if (player.hasItemInSlot(EquipmentSlot.MAINHAND) || player.hasItemInSlot(EquipmentSlot.OFFHAND))
return Optional.of(Component.translatable("controlify.guide.swap_hands")); return Optional.of(Component.translatable("controlify.guide.swap_hands"));
return Optional.empty(); return Optional.empty();
}); });
registerGuideAction(controller.bindings().PICK_BLOCK, ActionLocation.RIGHT, (client, player, level, hitResult, controller) -> { registerGuideAction(controller.bindings().PICK_BLOCK, ActionLocation.RIGHT, (ctx) -> {
if (hitResult.getType() == HitResult.Type.BLOCK && player.isCreative()) if (ctx.hitResult().getType() == HitResult.Type.BLOCK && ctx.player().isCreative())
return Optional.of(Component.translatable("controlify.guide.pick_block")); return Optional.of(Component.translatable("controlify.guide.pick_block"));
return Optional.empty(); return Optional.empty();
}); });
@ -248,7 +253,7 @@ public class InGameButtonGuide implements ButtonGuideRegistry {
private record GuideActionSupplier(ControllerBinding<?> binding, ActionLocation location, ActionPriority priority, GuideActionNameSupplier nameSupplier) { private record GuideActionSupplier(ControllerBinding<?> binding, ActionLocation location, ActionPriority priority, GuideActionNameSupplier nameSupplier) {
public Optional<GuideAction> supply(Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, Controller<?, ?> controller) { public Optional<GuideAction> supply(Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, Controller<?, ?> controller) {
return nameSupplier.supply(client, player, level, hitResult, controller) return nameSupplier.supply(new IngameGuideContext(client, player, level, hitResult, controller))
.map(name -> new GuideAction(binding, name, location, priority)); .map(name -> new GuideAction(binding, name, location, priority));
} }
} }

View File

@ -1,4 +1,4 @@
package dev.isxander.controlify.mixins.feature.guide; package dev.isxander.controlify.mixins.feature.guide.ingame;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode; import dev.isxander.controlify.InputMode;

View File

@ -1,4 +1,4 @@
package dev.isxander.controlify.mixins.feature.guide; package dev.isxander.controlify.mixins.feature.guide.ingame;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;

View File

@ -0,0 +1,86 @@
package dev.isxander.controlify.mixins.feature.guide.screen;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.gui.ButtonGuideRenderer;
import dev.isxander.controlify.screenop.ComponentProcessor;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
import dev.isxander.controlify.screenop.compat.vanilla.AbstractButtonComponentProcessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.contents.TranslatableContents;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Set;
@Mixin(AbstractButton.class)
public abstract class AbstractButtonMixin extends AbstractWidgetMixin implements ButtonGuideRenderer<AbstractButton> {
@Unique private RenderData<AbstractButton> renderData = null;
@Inject(method = "renderString", at = @At("RETURN"))
private void renderButtonGuide(PoseStack matrices, Font renderer, int color, CallbackInfo ci) {
if (shouldRender()) {
switch (renderData.position()) {
case LEFT -> getBind().draw(matrices, getX() - getBind().drawSize().width() - 1, getY() + getHeight() / 2);
case RIGHT -> getBind().draw(matrices, getX() + getWidth() + 1, getY() + getHeight() / 2);
case TEXT -> {
Font font = Minecraft.getInstance().font;
int x;
if (font.width(getMessage()) > getWidth()) {
x = getX();
} else {
x = getX() + getWidth() / 2 - font.width(getMessage()) / 2 - getBind().drawSize().width();
}
getBind().draw(matrices, x, getY() + getHeight() / 2);
}
}
}
}
@Inject(method = "renderString", at = @At("HEAD"))
private void shiftXOffset(PoseStack matrices, Font renderer, int color, CallbackInfo ci) {
matrices.pushPose();
if (!shouldRender() || Minecraft.getInstance().font.width(getMessage()) > getWidth() || renderData.position() != ButtonRenderPosition.TEXT) return;
matrices.translate(getBind().drawSize().width() / 2f, 0, 0);
}
@Inject(method = "renderString", at = @At("RETURN"))
private void finishShiftXOffset(PoseStack matrices, Font renderer, int color, CallbackInfo ci) {
matrices.popPose();
}
@Override
protected int shiftDrawSize(int x) {
if (!shouldRender() || Minecraft.getInstance().font.width(getMessage()) < getWidth() || renderData.position() != ButtonRenderPosition.TEXT) return x;
return x + getBind().drawSize().width();
}
@Override
public void setButtonGuide(RenderData<AbstractButton> renderData) {
this.renderData = renderData;
}
private boolean shouldRender() {
return renderData != null
&& this.isActive()
&& Controlify.instance().currentInputMode() == InputMode.CONTROLLER
&& Controlify.instance().currentController().config().showScreenGuide
&& !renderData.binding().apply(Controlify.instance().currentController().bindings()).unbound()
&& renderData.renderPredicate().shouldDisplay((AbstractButton) (Object) this);
}
private IBind<?> getBind() {
return renderData.binding().apply(Controlify.instance().currentController().bindings()).currentBind();
}
}

View File

@ -0,0 +1,32 @@
package dev.isxander.controlify.mixins.feature.guide.screen;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.gui.GuiComponent;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(AbstractWidget.class)
public abstract class AbstractWidgetMixin extends GuiComponent {
@Shadow public abstract int getX();
@Shadow public abstract int getY();
@Shadow public abstract int getHeight();
@Shadow public abstract Component getMessage();
@Shadow public abstract int getWidth();
@Shadow public abstract boolean isActive();
@ModifyArg(method = "renderScrollingString(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/gui/Font;II)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/AbstractWidget;renderScrollingString(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIII)V"), index = 3)
protected int shiftDrawSize(int x) {
return x;
}
}

View File

@ -0,0 +1,57 @@
package dev.isxander.controlify.mixins.feature.guide.screen;
import com.google.common.collect.ImmutableList;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.InputMode;
import dev.isxander.controlify.bindings.IBind;
import dev.isxander.controlify.compatibility.ControlifyCompat;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.gui.DrawSize;
import net.minecraft.client.gui.components.TabButton;
import net.minecraft.client.gui.components.tabs.TabNavigationBar;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TabNavigationBar.class)
public class TabNavigationBarMixin {
@Shadow @Final private ImmutableList<TabButton> tabButtons;
@Shadow private int width;
@Inject(method = "render", at = @At("RETURN"))
private void renderControllerButtonOverlay(PoseStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) {
if (Controlify.instance().currentInputMode() == InputMode.CONTROLLER) {
var controller = Controlify.instance().currentController();
if (controller.config().showScreenGuide) {
this.renderControllerButtonOverlay(matrices, controller);
}
}
}
private void renderControllerButtonOverlay(PoseStack matrices, Controller<?, ?> controller) {
ControlifyCompat.ifBeginHudBatching();
TabButton firstTab = tabButtons.get(0);
TabButton lastTab = tabButtons.get(tabButtons.size() - 1);
IBind<?> prevBind = controller.bindings().GUI_PREV_TAB.currentBind();
DrawSize prevBindDrawSize = prevBind.drawSize();
int firstButtonX = Math.max(firstTab.getX() - 2 - prevBindDrawSize.width(), firstTab.getX() / 2 - prevBindDrawSize.width() / 2);
int firstButtonY = 12;
prevBind.draw(matrices, firstButtonX, firstButtonY);
IBind<?> nextBind = controller.bindings().GUI_NEXT_TAB.currentBind();
DrawSize nextBindDrawSize = nextBind.drawSize();
int lastButtonEnd = lastTab.getX() + lastTab.getWidth();
int lastButtonX = Math.min(lastTab.getX() + lastTab.getWidth() + 2, lastButtonEnd + (width - lastButtonEnd) / 2 - nextBindDrawSize.width() / 2);
int lastButtonY = 12;
nextBind.draw(matrices, lastButtonX, lastButtonY);
ControlifyCompat.ifEndHudBatching();
}
}

View File

@ -0,0 +1,40 @@
package dev.isxander.controlify.mixins.feature.screenop.vanilla;
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
import dev.isxander.controlify.gui.ButtonGuideRenderer;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.screenop.compat.vanilla.CreateWorldScreenProcessor;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.client.gui.layouts.LayoutElement;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(CreateWorldScreen.class)
public abstract class CreateWorldScreenMixin implements ScreenProcessorProvider {
@Shadow protected abstract void onCreate();
@Unique private final ScreenProcessor<CreateWorldScreen> processor = new CreateWorldScreenProcessor((CreateWorldScreen) (Object) this, this::onCreate);
@ModifyArg(method = "init()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/layouts/GridLayout$RowHelper;addChild(Lnet/minecraft/client/gui/layouts/LayoutElement;)Lnet/minecraft/client/gui/layouts/LayoutElement;", ordinal = 1))
private LayoutElement modifyCancelButton(LayoutElement button) {
ButtonGuideRenderer.registerBindingForButton((AbstractButton) button, bindings -> bindings.GUI_BACK, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
return button;
}
@ModifyArg(method = "init()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/layouts/GridLayout$RowHelper;addChild(Lnet/minecraft/client/gui/layouts/LayoutElement;)Lnet/minecraft/client/gui/layouts/LayoutElement;", ordinal = 0))
private LayoutElement modifyCreateButton(LayoutElement button) {
ButtonGuideRenderer.registerBindingForButton((AbstractButton) button, bindings -> bindings.GUI_ABSTRACT_ACTION_1, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
return button;
}
@Override
public ScreenProcessor<?> screenProcessor() {
return processor;
}
}

View File

@ -1,10 +1,18 @@
package dev.isxander.controlify.mixins.feature.screenop.vanilla; package dev.isxander.controlify.mixins.feature.screenop.vanilla;
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
import dev.isxander.controlify.gui.ButtonGuideRenderer;
import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.screenop.compat.vanilla.SelectWorldScreenProcessor; import dev.isxander.controlify.screenop.compat.vanilla.SelectWorldScreenProcessor;
import net.minecraft.client.gui.components.AbstractButton;
import net.minecraft.client.gui.components.Renderable;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(SelectWorldScreen.class) @Mixin(SelectWorldScreen.class)
public class SelectWorldScreenMixin implements ScreenProcessorProvider { public class SelectWorldScreenMixin implements ScreenProcessorProvider {
@ -14,4 +22,16 @@ public class SelectWorldScreenMixin implements ScreenProcessorProvider {
public ScreenProcessor<?> screenProcessor() { public ScreenProcessor<?> screenProcessor() {
return controlify$processor; return controlify$processor;
} }
@ModifyArg(method = "init()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/worldselection/SelectWorldScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;", ordinal = 5))
private <T extends GuiEventListener & Renderable> T modifyCancelButton(T button) {
ButtonGuideRenderer.registerBindingForButton((AbstractButton) button, bindings -> bindings.GUI_BACK, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
return button;
}
@ModifyArg(method = "init()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/worldselection/SelectWorldScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;", ordinal = 1))
private <T extends GuiEventListener & Renderable> T modifyCreateButton(T button) {
ButtonGuideRenderer.registerBindingForButton((AbstractButton) button, bindings -> bindings.GUI_ABSTRACT_ACTION_1, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
return button;
}
} }

View File

@ -6,6 +6,7 @@ import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.ScreenAccessor; import dev.isxander.controlify.mixins.feature.screenop.vanilla.ScreenAccessor;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.TabNavigationBarAccessor; import dev.isxander.controlify.mixins.feature.screenop.vanilla.TabNavigationBarAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.ComponentPath;
import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.components.tabs.Tab; import net.minecraft.client.gui.components.tabs.Tab;
@ -13,6 +14,8 @@ import net.minecraft.client.gui.components.tabs.TabNavigationBar;
import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.FocusNavigationEvent;
import net.minecraft.client.gui.navigation.ScreenDirection; import net.minecraft.client.gui.navigation.ScreenDirection;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.sounds.SoundEvents;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.util.*; import java.util.*;
@ -20,6 +23,7 @@ import java.util.*;
public class ScreenProcessor<T extends Screen> { public class ScreenProcessor<T extends Screen> {
public final T screen; public final T screen;
protected int lastMoved = 0; protected int lastMoved = 0;
protected final Minecraft minecraft = Minecraft.getInstance();
public ScreenProcessor(T screen) { public ScreenProcessor(T screen) {
this.screen = screen; this.screen = screen;
@ -101,8 +105,10 @@ public class ScreenProcessor<T extends Screen> {
if (controller.bindings().GUI_PRESS.justPressed()) if (controller.bindings().GUI_PRESS.justPressed())
screen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0); screen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0);
if (controller.bindings().GUI_BACK.justPressed()) if (controller.bindings().GUI_BACK.justPressed()) {
this.playClackSound();
screen.onClose(); screen.onClose();
}
} }
protected void handleVMouseNavigation(Controller<?, ?> controller) { protected void handleVMouseNavigation(Controller<?, ?> controller) {
@ -174,4 +180,8 @@ public class ScreenProcessor<T extends Screen> {
return tree; return tree;
} }
protected void playClackSound() {
minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
}
} }

View File

@ -0,0 +1,24 @@
package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
public class CreateWorldScreenProcessor extends ScreenProcessor<CreateWorldScreen> {
private final Runnable onCreateButton;
public CreateWorldScreenProcessor(CreateWorldScreen screen, Runnable onCreateButton) {
super(screen);
this.onCreateButton = onCreateButton;
}
@Override
protected void handleButtons(Controller<?, ?> controller) {
if (controller.bindings().GUI_ABSTRACT_ACTION_1.justPressed()) {
this.onCreateButton.run();
this.playClackSound();
}
super.handleButtons(controller);
}
}

View File

@ -3,7 +3,9 @@ package dev.isxander.controlify.screenop.compat.vanilla;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.mixins.feature.screenop.vanilla.SelectWorldScreenAccessor; import dev.isxander.controlify.mixins.feature.screenop.vanilla.SelectWorldScreenAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
public class SelectWorldScreenProcessor extends ScreenProcessor<SelectWorldScreen> { public class SelectWorldScreenProcessor extends ScreenProcessor<SelectWorldScreen> {
@ -13,6 +15,12 @@ public class SelectWorldScreenProcessor extends ScreenProcessor<SelectWorldScree
@Override @Override
protected void handleButtons(Controller<?, ?> controller) { protected void handleButtons(Controller<?, ?> controller) {
if (controller.bindings().GUI_ABSTRACT_ACTION_1.justPressed()) {
this.playClackSound();
CreateWorldScreen.openFresh(Minecraft.getInstance(), screen);
return;
}
if (screen.getFocused() != null && screen.getFocused() instanceof Button) { if (screen.getFocused() != null && screen.getFocused() instanceof Button) {
if (controller.bindings().GUI_BACK.justPressed()) { if (controller.bindings().GUI_BACK.justPressed()) {
screen.setFocused(((SelectWorldScreenAccessor) screen).getList()); screen.setFocused(((SelectWorldScreenAccessor) screen).getList());

View File

@ -20,8 +20,10 @@
"controlify.gui.toggle_sprint.tooltip": "How the state of the sprint button behaves.", "controlify.gui.toggle_sprint.tooltip": "How the state of the sprint button behaves.",
"controlify.gui.auto_jump": "Auto Jump", "controlify.gui.auto_jump": "Auto Jump",
"controlify.gui.auto_jump.tooltip": "If the player should automatically jump when you reach a block.", "controlify.gui.auto_jump.tooltip": "If the player should automatically jump when you reach a block.",
"controlify.gui.show_guide": "Show Button Guide", "controlify.gui.show_ingame_guide": "Show Ingame Button Guide",
"controlify.gui.show_guide.tooltip": "Show a HUD in-game displaying actions you can do with controller buttons.", "controlify.gui.show_ingame_guide.tooltip": "Show a HUD in-game displaying actions you can do with controller buttons.",
"controlify.gui.show_screen_guide": "Show Screen Button Guide",
"controlify.gui.show_screen_guide.tooltip": "Show various helpers in GUIs to assist in using controller input.",
"controlify.gui.vmouse_sensitivity": "Virtual Mouse Sensitivity", "controlify.gui.vmouse_sensitivity": "Virtual Mouse Sensitivity",
"controlify.gui.vmouse_sensitivity.tooltip": "How fast the virtual mouse moves.", "controlify.gui.vmouse_sensitivity.tooltip": "How fast the virtual mouse moves.",
"controlify.gui.chat_screen_offset": "On-screen keyboard height", "controlify.gui.chat_screen_offset": "On-screen keyboard height",
@ -55,6 +57,8 @@
"controlify.gui.button": "Controller Settings...", "controlify.gui.button": "Controller Settings...",
"controlify.gui.controller_unavailable": "Controller unavailable and cannot be edited.",
"controlify.toast.vmouse_enabled.title": "Virtual Mouse Enabled", "controlify.toast.vmouse_enabled.title": "Virtual Mouse Enabled",
"controlify.toast.vmouse_enabled.description": "Controlify virtual mouse is now enabled for this screen.", "controlify.toast.vmouse_enabled.description": "Controlify virtual mouse is now enabled for this screen.",
"controlify.toast.vmouse_disabled.title": "Virtual Mouse Disabled", "controlify.toast.vmouse_disabled.title": "Virtual Mouse Disabled",

View File

@ -0,0 +1,176 @@
{
"axes": [
{
"ids": [0, 1],
"identifier": "ps4_left_stick",
"deadzone": true,
"rest": 0.0,
"axis_names": [
["right", "left"],
["down", "up"]
]
},
{
"ids": [2, 5],
"identifier": "ps4_right_stick",
"deadzone": true,
"axis_names": [
["right", "left"],
["down", "up"]
],
"rest": 0.0
},
{
"ids": [3],
"identifier": "ps4_left_trigger",
"rest": 0.0,
"deadzone": false,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
},
{
"ids": [4],
"identifier": "ps4_right_trigger",
"rest": 0.0,
"deadzone": false,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
},
{
"ids": [6, 7],
"identifier": "xbox_left_stick",
"deadzone": true,
"rest": 0.0,
"axis_names": [
["right", "left"],
["down", "up"]
]
},
{
"ids": [8, 9],
"identifier": "xbox_right_stick",
"deadzone": true,
"axis_names": [
["right", "left"],
["down", "up"]
],
"rest": 0.0
},
{
"ids": [10],
"identifier": "xbox_left_trigger",
"deadzone": false,
"rest": 0.0,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
},
{
"ids": [11],
"identifier": "xbox_right_trigger",
"deadzone": false,
"rest": 0.0,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
}
],
"buttons": [
{
"button": 0,
"name": "a"
},
{
"button": 1,
"name": "b"
},
{
"button": 2,
"name": "x"
},
{
"button": 3,
"name": "y"
},
{
"button": 4,
"name": "left_bumper"
},
{
"button": 5,
"name": "right_bumper"
},
{
"button": 6,
"name": "back"
},
{
"button": 7,
"name": "start"
},
{
"button": 8,
"name": "left_stick"
},
{
"button": 9,
"name": "right_stick"
},
{
"button": 10,
"name": "a1"
},
{
"button": 11,
"name": "b1"
},
{
"button": 12,
"name": "x1"
},
{
"button": 13,
"name": "y1"
},
{
"button": 14,
"name": "left_bumper1"
},
{
"button": 15,
"name": "right_bumper1"
},
{
"button": 16,
"name": "back1"
},
{
"button": 17,
"name": "start1"
},
{
"button": 18,
"name": "left_stick1"
},
{
"button": 19,
"name": "right_stick1"
}
],
"hats": [
{
"hat": 0,
"name": "dpad_xbox"
},
{
"hat": 1,
"name": "dpad_ps4"
}
]
}

View File

@ -1,92 +0,0 @@
{
"axes": [
{
"ids": [0, 1],
"identifier": "left_stick",
"deadzone": true,
"rest": 0.0,
"axis_names": [
["right", "left"],
["down", "up"]
]
},
{
"ids": [2, 3],
"identifier": "right_stick",
"deadzone": true,
"axis_names": [
["right", "left"],
["down", "up"]
],
"rest": 0.0
},
{
"ids": [4],
"identifier": "left_trigger",
"deadzone": false,
"rest": 0.0,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
},
{
"ids": [5],
"identifier": "right_trigger",
"deadzone": false,
"rest": 0.0,
"range": [0.0, 1.0],
"axis_names": [
["down", "up"]
]
}
],
"buttons": [
{
"button": 0,
"name": "a"
},
{
"button": 1,
"name": "b"
},
{
"button": 2,
"name": "x"
},
{
"button": 3,
"name": "y"
},
{
"button": 4,
"name": "left_bumper"
},
{
"button": 5,
"name": "right_bumper"
},
{
"button": 6,
"name": "back"
},
{
"button": 7,
"name": "start"
},
{
"button": 8,
"name": "left_stick"
},
{
"button": 9,
"name": "right_stick"
}
],
"hats": [
{
"hat": 0,
"name": "dpad"
}
]
}

View File

@ -25,14 +25,17 @@
"feature.bind.KeyMappingAccessor", "feature.bind.KeyMappingAccessor",
"feature.chatkbheight.ChatComponentMixin", "feature.chatkbheight.ChatComponentMixin",
"feature.chatkbheight.ChatScreenMixin", "feature.chatkbheight.ChatScreenMixin",
"feature.guide.ClientPacketListenerMixin", "feature.guide.ingame.ClientPacketListenerMixin",
"feature.guide.GuiMixin", "feature.guide.ingame.GuiMixin",
"feature.guide.screen.AbstractButtonMixin",
"feature.guide.screen.AbstractWidgetMixin",
"feature.guide.screen.TabNavigationBarMixin",
"feature.screenop.vanilla.AbstractButtonMixin", "feature.screenop.vanilla.AbstractButtonMixin",
"feature.screenop.vanilla.AbstractContainerEventHandlerMixin", "feature.screenop.vanilla.AbstractContainerEventHandlerMixin",
"feature.screenop.vanilla.AbstractSelectionListMixin", "feature.screenop.vanilla.AbstractSelectionListMixin",
"feature.screenop.vanilla.AbstractSliderButtonMixin", "feature.screenop.vanilla.AbstractSliderButtonMixin",
"feature.screenop.vanilla.ContainerObjectSelectionListEntryMixin", "feature.screenop.vanilla.ContainerObjectSelectionListEntryMixin",
"feature.screenop.vanilla.CreateWorldScreenMixin",
"feature.screenop.vanilla.CreativeModeInventoryScreenAccessor", "feature.screenop.vanilla.CreativeModeInventoryScreenAccessor",
"feature.screenop.vanilla.CreativeModeInventoryScreenMixin", "feature.screenop.vanilla.CreativeModeInventoryScreenMixin",
"feature.screenop.vanilla.JoinMultiplayerScreenAccessor", "feature.screenop.vanilla.JoinMultiplayerScreenAccessor",

View File

@ -73,8 +73,8 @@ public class ClientTestHelper {
} }
@Override @Override
public String guid() { public int joystickId() {
return "FAKE"; return -1;
} }
@Override @Override