YACL 3.x port with new controller carousel select screen
@ -8,12 +8,12 @@ machete = "2.+"
|
|||||||
grgit = "5.0.+"
|
grgit = "5.0.+"
|
||||||
blossom = "1.3.+"
|
blossom = "1.3.+"
|
||||||
|
|
||||||
minecraft = "1.20-pre2"
|
minecraft = "1.20-pre6"
|
||||||
quilt_mappings = "1"
|
quilt_mappings = "1"
|
||||||
fabric_loader = "0.14.19"
|
fabric_loader = "0.14.19"
|
||||||
fabric_api = "0.81.2+1.20"
|
fabric_api = "0.81.2+1.20"
|
||||||
mixin_extras = "0.2.0-beta.8"
|
mixin_extras = "0.2.0-beta.8"
|
||||||
yet_another_config_lib = "2.5.1-beta.1+1.20"
|
yet_another_config_lib = "3.0.0-beta.4+1.20"
|
||||||
mod_menu = "7.0.0-beta.2"
|
mod_menu = "7.0.0-beta.2"
|
||||||
hid4java = "0.7.0"
|
hid4java = "0.7.0"
|
||||||
quilt_json5 = "1.0.3"
|
quilt_json5 = "1.0.3"
|
||||||
|
@ -5,6 +5,7 @@ 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.api.entrypoint.ControlifyEntrypoint;
|
||||||
import dev.isxander.controlify.config.gui.ControllerBindHandler;
|
import dev.isxander.controlify.config.gui.ControllerBindHandler;
|
||||||
|
import dev.isxander.controlify.config.gui.ControllerCarouselScreen;
|
||||||
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.sdl2.SDL2NativesManager;
|
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||||
@ -24,8 +25,6 @@ import dev.isxander.controlify.utils.ToastUtils;
|
|||||||
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
||||||
import dev.isxander.controlify.wireless.LowBatteryNotifier;
|
import dev.isxander.controlify.wireless.LowBatteryNotifier;
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||||
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
|
|
||||||
import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
|
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
import net.minecraft.CrashReport;
|
import net.minecraft.CrashReport;
|
||||||
import net.minecraft.CrashReportCategory;
|
import net.minecraft.CrashReportCategory;
|
||||||
@ -335,6 +334,10 @@ public class Controlify implements ControlifyApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canDiscoverControllers = false;
|
canDiscoverControllers = false;
|
||||||
|
|
||||||
|
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||||
|
controllerListScreen.refreshControllers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onControllerDisconnect(int jid) {
|
private void onControllerDisconnect(int jid) {
|
||||||
@ -352,6 +355,10 @@ 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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||||
|
controllerListScreen.refreshControllers();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ 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 dev.isxander.yacl.api.Option;
|
import dev.isxander.yacl.api.Option;
|
||||||
|
import dev.isxander.yacl.api.OptionDescription;
|
||||||
import net.minecraft.client.KeyMapping;
|
import net.minecraft.client.KeyMapping;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.locale.Language;
|
import net.minecraft.locale.Language;
|
||||||
@ -171,12 +172,14 @@ public class ControllerBindingImpl<T extends ControllerState> implements Control
|
|||||||
Option.Builder<IBind<T>> option = Option.createBuilder((Class<IBind<T>>) (Class<?>) IBind.class)
|
Option.Builder<IBind<T>> option = Option.createBuilder((Class<IBind<T>>) (Class<?>) IBind.class)
|
||||||
.name(name())
|
.name(name())
|
||||||
.binding(defaultBind(), this::currentBind, this::setCurrentBind)
|
.binding(defaultBind(), this::currentBind, this::setCurrentBind)
|
||||||
.tooltip(this.description());
|
.description(OptionDescription.of(this.description()));
|
||||||
|
|
||||||
if (controller instanceof GamepadController gamepad) {
|
if (controller instanceof GamepadController gamepad) {
|
||||||
((Option.Builder<IBind<GamepadState>>) (Object) option).controller(opt -> new GamepadBindController(opt, gamepad));
|
((Option.Builder<IBind<GamepadState>>) (Object) option).customController(opt -> new GamepadBindController(opt, gamepad));
|
||||||
} else if (controller instanceof JoystickController<?> joystick) {
|
} else if (controller instanceof JoystickController<?> joystick) {
|
||||||
((Option.Builder<IBind<JoystickState>>) (Object) option).controller(opt -> new JoystickBindController(opt, joystick));
|
((Option.Builder<IBind<JoystickState>>) (Object) option).customController(opt -> new JoystickBindController(opt, joystick));
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unknown controller type: " + controller.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
|
@ -15,19 +15,9 @@ public class YACLScreenProcessor extends ScreenProcessor<YACLScreen> {
|
|||||||
@Override
|
@Override
|
||||||
protected void handleButtons(Controller<?, ?> controller) {
|
protected void handleButtons(Controller<?, ?> controller) {
|
||||||
if (controller.bindings().GUI_ABSTRACT_ACTION_1.justPressed()) {
|
if (controller.bindings().GUI_ABSTRACT_ACTION_1.justPressed()) {
|
||||||
this.playClackSound();
|
playClackSound();
|
||||||
screen.finishedSaveButton.onPress();
|
// if (screen.tabManager.getCurrentTab() instanceof )
|
||||||
}
|
// screen.finishedSaveButton.onPress();
|
||||||
|
|
||||||
if (controller.bindings().GUI_NEXT_TAB.justPressed()) {
|
|
||||||
var idx = screen.getCurrentCategoryIdx() + 1;
|
|
||||||
if (idx >= screen.config.categories().size()) idx = 0;
|
|
||||||
screen.changeCategory(idx);
|
|
||||||
}
|
|
||||||
if (controller.bindings().GUI_PREV_TAB.justPressed()) {
|
|
||||||
var idx = screen.getCurrentCategoryIdx() - 1;
|
|
||||||
if (idx < 0) idx = screen.config.categories().size() - 1;
|
|
||||||
screen.changeCategory(idx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.handleButtons(controller);
|
super.handleButtons(controller);
|
||||||
@ -35,11 +25,11 @@ public class YACLScreenProcessor extends ScreenProcessor<YACLScreen> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onWidgetRebuild() {
|
public void onWidgetRebuild() {
|
||||||
ButtonGuideApi.addGuideToButton(screen.finishedSaveButton, bindings -> bindings.GUI_ABSTRACT_ACTION_1, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
|
//ButtonGuideApi.addGuideToButton(screen.finishedSaveButton, bindings -> bindings.GUI_ABSTRACT_ACTION_1, ButtonRenderPosition.TEXT, ButtonGuidePredicate.ALWAYS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setInitialFocus() {
|
protected void setInitialFocus() {
|
||||||
screen.setFocused(screen.optionList);
|
// screen.setFocused(screen.optionList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,325 @@
|
|||||||
|
package dev.isxander.controlify.config.gui;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.mojang.blaze3d.platform.InputConstants;
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.ControllerManager;
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||||
|
import dev.isxander.controlify.gui.screen.SDLOnboardingScreen;
|
||||||
|
import dev.isxander.controlify.utils.Animator;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.client.gui.ComponentPath;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.components.Button;
|
||||||
|
import net.minecraft.client.gui.components.Renderable;
|
||||||
|
import net.minecraft.client.gui.components.events.AbstractContainerEventHandler;
|
||||||
|
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||||
|
import net.minecraft.client.gui.layouts.FrameLayout;
|
||||||
|
import net.minecraft.client.gui.layouts.GridLayout;
|
||||||
|
import net.minecraft.client.gui.narration.NarratableEntry;
|
||||||
|
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||||
|
import net.minecraft.client.gui.navigation.FocusNavigationEvent;
|
||||||
|
import net.minecraft.client.gui.navigation.ScreenAxis;
|
||||||
|
import net.minecraft.client.gui.navigation.ScreenDirection;
|
||||||
|
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
|
||||||
|
import net.minecraft.network.chat.CommonComponents;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.FastColor;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ControllerCarouselScreen extends Screen {
|
||||||
|
public static final ResourceLocation CHECKMARK = new ResourceLocation("textures/gui/checkmark.png");
|
||||||
|
|
||||||
|
private final Screen parent;
|
||||||
|
|
||||||
|
private List<CarouselEntry> carouselEntries = null;
|
||||||
|
private int carouselIndex;
|
||||||
|
private Animator animator;
|
||||||
|
|
||||||
|
private ControllerCarouselScreen(Screen parent) {
|
||||||
|
super(Component.literal("Controllers"));
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Screen createConfigScreen(Screen parent) {
|
||||||
|
var controlify = Controlify.instance();
|
||||||
|
|
||||||
|
if (!controlify.config().globalSettings().vibrationOnboarded) {
|
||||||
|
return new SDLOnboardingScreen(() -> new ControllerCarouselScreen(parent), yes -> {
|
||||||
|
if (yes) {
|
||||||
|
SDL2NativesManager.initialise();
|
||||||
|
|
||||||
|
if (controlify.config().globalSettings().delegateSetup) {
|
||||||
|
controlify.discoverControllers();
|
||||||
|
controlify.config().globalSettings().delegateSetup = false;
|
||||||
|
controlify.config().save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (Controlify.instance().config().globalSettings().delegateSetup) {
|
||||||
|
controlify.discoverControllers();
|
||||||
|
controlify.config().globalSettings().delegateSetup = false;
|
||||||
|
controlify.config().save();
|
||||||
|
}
|
||||||
|
return new ControllerCarouselScreen(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
refreshControllers();
|
||||||
|
|
||||||
|
GridLayout grid = new GridLayout().columnSpacing(10);
|
||||||
|
GridLayout.RowHelper rowHelper = grid.createRowHelper(2);
|
||||||
|
rowHelper.addChild(Button.builder(Component.literal("Global Settings"), btn -> minecraft.setScreen(GlobalSettingsGui.createGlobalSettingsScreen(this))).build());
|
||||||
|
rowHelper.addChild(Button.builder(CommonComponents.GUI_DONE, btn -> this.onClose()).build());
|
||||||
|
grid.visitWidgets(widget -> {
|
||||||
|
widget.setTabOrderGroup(1);
|
||||||
|
this.addRenderableWidget(widget);
|
||||||
|
});
|
||||||
|
grid.arrangeElements();
|
||||||
|
FrameLayout.centerInRectangle(grid, 0, this.height - 36, this.width, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshControllers() {
|
||||||
|
if (carouselEntries != null) {
|
||||||
|
carouselEntries.forEach(this::removeWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
carouselEntries = ControllerManager.getConnectedControllers().stream()
|
||||||
|
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
|
||||||
|
.peek(this::addRenderableWidget)
|
||||||
|
.toList();
|
||||||
|
carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0);
|
||||||
|
if (!carouselEntries.isEmpty())
|
||||||
|
carouselEntries.get(carouselIndex).overlayColor = 0;
|
||||||
|
|
||||||
|
float offsetX = (this.width / 2f) * -(carouselIndex - 1) - this.width / 6f;
|
||||||
|
for (int i = 0; i < carouselEntries.size(); i++) {
|
||||||
|
CarouselEntry entry = carouselEntries.get(i);
|
||||||
|
entry.setX(offsetX + (this.width / 2f) * i);
|
||||||
|
entry.setY(i == carouselIndex ? 20 : 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
|
||||||
|
renderDirtBackground(graphics);
|
||||||
|
|
||||||
|
int footerY = Mth.roundToward(this.height - 36 - 2, 2);
|
||||||
|
graphics.blit(CreateWorldScreen.FOOTER_SEPERATOR, 0, footerY, 0.0F, 0.0F, this.width, 2, 32, 2);
|
||||||
|
|
||||||
|
graphics.setColor(0.5f, 0.5f, 0.5f, 1f);
|
||||||
|
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0f, 0f, this.width, footerY, 32, 32);
|
||||||
|
graphics.setColor(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
|
if (animator != null && !animator.isDone()) {
|
||||||
|
animator.tick(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (carouselEntries.isEmpty()) {
|
||||||
|
graphics.drawCenteredString(font, Component.literal("No controllers connected."), this.width / 2, (this.height - 36) / 2 - 10, 0xFFAAAAAA);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.render(graphics, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderDirtBackground(GuiGraphics graphics) {
|
||||||
|
int scale = 32;
|
||||||
|
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0.0F, 0.0F, this.width, this.height, scale, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case InputConstants.KEY_RIGHT -> {
|
||||||
|
if (carouselEntries.stream().anyMatch(CarouselEntry::isFocused))
|
||||||
|
focusOnEntry(Math.min(carouselEntries.size() - 1, carouselIndex + 1));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case InputConstants.KEY_LEFT -> {
|
||||||
|
if (carouselEntries.stream().anyMatch(CarouselEntry::isFocused))
|
||||||
|
focusOnEntry(Math.max(0, carouselIndex - 1));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void focusOnEntry(int index) {
|
||||||
|
if (animator != null && !animator.isDone())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int diff = index - carouselIndex;
|
||||||
|
if (diff == 0) return;
|
||||||
|
|
||||||
|
carouselIndex = index;
|
||||||
|
|
||||||
|
animator = new Animator(10, x -> x < 0.5f ? 4 * x * x * x : 1 - (float)Math.pow(-2 * x + 2, 3) / 2);
|
||||||
|
for (CarouselEntry entry : carouselEntries) {
|
||||||
|
boolean selected = carouselEntries.indexOf(entry) == index;
|
||||||
|
animator.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f));
|
||||||
|
animator.addConsumer(entry::setY, entry.getY(), selected ? 20f : 10f);
|
||||||
|
animator.addConsumer(t -> entry.overlayColor = FastColor.ARGB32.lerp(t, entry.overlayColor, selected ? 0 : 0x90000000), 0f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose() {
|
||||||
|
minecraft.setScreen(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CarouselEntry extends AbstractContainerEventHandler implements Renderable, NarratableEntry {
|
||||||
|
private int x, y;
|
||||||
|
private final int width, height;
|
||||||
|
|
||||||
|
private float translationX, translationY;
|
||||||
|
|
||||||
|
private final Controller<?, ?> controller;
|
||||||
|
private final boolean hasNickname;
|
||||||
|
|
||||||
|
private Button useControllerButton;
|
||||||
|
private Button settingsButton;
|
||||||
|
|
||||||
|
private int overlayColor = 0x90000000;
|
||||||
|
|
||||||
|
private CarouselEntry(Controller<?, ?> controller, int width, int height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
this.controller = controller;
|
||||||
|
this.hasNickname = this.controller.config().customName != null;
|
||||||
|
|
||||||
|
this.settingsButton = Button.builder(Component.literal("Settings"), btn -> minecraft.setScreen(ControllerConfigGui.generateConfigScreen(ControllerCarouselScreen.this, controller))).width(getWidth() / 2 - 4).build();
|
||||||
|
this.useControllerButton = Button.builder(Component.literal("Use"), btn -> Controlify.instance().setCurrentController(controller)).width(settingsButton.getWidth()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
|
||||||
|
graphics.pose().pushPose();
|
||||||
|
graphics.pose().translate(translationX, translationY, 0);
|
||||||
|
|
||||||
|
graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, x, y, 0, 0f, 0f, width, height, 32, 32);
|
||||||
|
|
||||||
|
graphics.renderOutline(x, y, width, height, 0x5AFFFFFF);
|
||||||
|
useControllerButton.render(graphics, mouseX, mouseY, delta);
|
||||||
|
settingsButton.render(graphics, mouseX, mouseY, delta);
|
||||||
|
|
||||||
|
graphics.drawCenteredString(font, controller.name(), x + width / 2, y + height - 26 - font.lineHeight - (hasNickname ? font.lineHeight + 1 : 0), 0xFFFFFF);
|
||||||
|
if (hasNickname) {
|
||||||
|
String nickname = controller.config().customName;
|
||||||
|
controller.config().customName = null;
|
||||||
|
graphics.drawCenteredString(font, controller.name(), x + width / 2, y + height - 26 - font.lineHeight, 0xAAAAAA);
|
||||||
|
controller.config().customName = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Controlify.instance().getCurrentController().orElse(null) == controller) {
|
||||||
|
graphics.blit(CHECKMARK, x + 4, y + 4, 0f, 0f, 9, 8, 9, 8);
|
||||||
|
graphics.drawString(font, Component.literal("Currently in use").withStyle(ChatFormatting.GREEN), x + 17, y + 4, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int iconWidth = width - 6;
|
||||||
|
// buttons 4px padding top currently in use controller name image padding
|
||||||
|
int iconHeight = height - 22 - 4 - font.lineHeight - 8 - (font.lineHeight * (hasNickname ? 2 : 1) + 1) - 6;
|
||||||
|
int iconSize = Mth.roundToward(Math.min(iconHeight, iconWidth), 2);
|
||||||
|
|
||||||
|
graphics.pose().pushPose();
|
||||||
|
graphics.pose().translate(x + width / 2 - iconSize / 2, y + font.lineHeight + 12 + iconHeight / 2 - iconSize / 2, 0);
|
||||||
|
graphics.pose().scale(iconSize / 64f, iconSize / 64f, 1);
|
||||||
|
graphics.blit(controller.icon(), 0, 0, 0f, 0f, 64, 64, 64, 64);
|
||||||
|
|
||||||
|
graphics.pose().translate(0, 0, 1);
|
||||||
|
graphics.fill(x, y, x + width, y + height, overlayColor);
|
||||||
|
|
||||||
|
graphics.pose().popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||||
|
if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) {
|
||||||
|
int index = carouselEntries.indexOf(this);
|
||||||
|
if (index != carouselIndex) {
|
||||||
|
if (animator == null || animator.isDone())
|
||||||
|
focusOnEntry(index);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.mouseClicked(mouseX, mouseY, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFocused(boolean focused) {
|
||||||
|
super.setFocused(focused);
|
||||||
|
if (focused) {
|
||||||
|
focusOnEntry(carouselEntries.indexOf(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<? extends GuiEventListener> children() {
|
||||||
|
return ImmutableList.of(useControllerButton, settingsButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setX(float x) {
|
||||||
|
this.x = (int)x;
|
||||||
|
this.settingsButton.setX((int)x + 2);
|
||||||
|
this.useControllerButton.setX(this.settingsButton.getX() + this.settingsButton.getWidth() + 2);
|
||||||
|
this.translationX = x - (int)x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setY(float y) {
|
||||||
|
this.y = (int)y;
|
||||||
|
this.useControllerButton.setY((int)y + getHeight() - 20 - 2);
|
||||||
|
this.settingsButton.setY(this.useControllerButton.getY());
|
||||||
|
this.translationY = y - (int)y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getX() {
|
||||||
|
return this.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getY() {
|
||||||
|
return this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public ComponentPath nextFocusPath(FocusNavigationEvent event) {
|
||||||
|
if (animator != null && !animator.isDone())
|
||||||
|
return null;
|
||||||
|
return super.nextFocusPath(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScreenRectangle getRectangle() {
|
||||||
|
return new ScreenRectangle(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NarrationPriority narrationPriority() {
|
||||||
|
return NarrationPriority.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateNarration(NarrationElementOutput builder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,465 @@
|
|||||||
|
package dev.isxander.controlify.config.gui;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.api.bind.ControllerBinding;
|
||||||
|
import dev.isxander.controlify.bindings.BindContext;
|
||||||
|
import dev.isxander.controlify.bindings.EmptyBind;
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.controlify.controller.ControllerConfig;
|
||||||
|
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||||
|
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||||
|
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||||
|
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||||
|
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
||||||
|
import dev.isxander.controlify.rumble.RumbleSource;
|
||||||
|
import dev.isxander.controlify.rumble.RumbleState;
|
||||||
|
import dev.isxander.yacl.api.*;
|
||||||
|
import dev.isxander.yacl.api.controller.BooleanControllerBuilder;
|
||||||
|
import dev.isxander.yacl.api.controller.FloatSliderControllerBuilder;
|
||||||
|
import dev.isxander.yacl.api.controller.StringControllerBuilder;
|
||||||
|
import dev.isxander.yacl.api.controller.TickBoxControllerBuilder;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.Util;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.network.chat.CommonComponents;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class ControllerConfigGui {
|
||||||
|
private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
|
||||||
|
private static final Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v);
|
||||||
|
|
||||||
|
public static Screen generateConfigScreen(Screen parent, Controller<?, ?> controller) {
|
||||||
|
ControllerConfig def = controller.defaultConfig();
|
||||||
|
ControllerConfig config = controller.config();
|
||||||
|
|
||||||
|
return YetAnotherConfigLib.createBuilder()
|
||||||
|
.title(Component.literal("Controlify"))
|
||||||
|
.category(createBasicCategory(controller, def, config))
|
||||||
|
.category(createAdvancedCategory(controller))
|
||||||
|
.category(createBindsCategory(controller))
|
||||||
|
.save(() -> Controlify.instance().config().save())
|
||||||
|
.build().generateScreen(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConfigCategory createBasicCategory(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
|
||||||
|
return ConfigCategory.createBuilder()
|
||||||
|
.name(Component.literal("Basic"))
|
||||||
|
.option(Option.<String>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.custom_name"))
|
||||||
|
.description(OptionDescription.of(Component.translatable("controlify.gui.custom_name.tooltip")))
|
||||||
|
.binding(def.customName == null ? "" : def.customName, () -> config.customName == null ? "" : config.customName, v -> config.customName = (v.equals("") ? null : v))
|
||||||
|
.controller(StringControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.group(makeSensitivityGroup(controller, def, config))
|
||||||
|
.group(makeControlsGroup(controller, def, config))
|
||||||
|
.group(makeAccessibilityGroup(controller, controller.defaultConfig(), controller.config()))
|
||||||
|
.group(makeDeadzoneGroup(controller, controller.defaultConfig(), controller.config()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeSensitivityGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
|
||||||
|
return OptionGroup.createBuilder()
|
||||||
|
.name(Component.literal("Sensitivity"))
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.horizontal_look_sensitivity"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.horizontal_look_sensitivity.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.horizontalLookSensitivity, () -> config.horizontalLookSensitivity, v -> config.horizontalLookSensitivity = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.vertical_look_sensitivity"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.vertical_look_sensitivity.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.verticalLookSensitivity, () -> config.verticalLookSensitivity, v -> config.verticalLookSensitivity = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.vmouse_sensitivity"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.vmouse_sensitivity.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.virtualMouseSensitivity, () -> config.virtualMouseSensitivity, v -> config.virtualMouseSensitivity = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0.1f, 2f).step(0.05f).valueFormatter(percentFormatter))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.reduce_aiming_sensitivity"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.reduce_aiming_sensitivity.tooltip"))
|
||||||
|
.webpImage(screenshot("reduce-aim-sensitivity.webp"))
|
||||||
|
.build())
|
||||||
|
.binding(def.reduceAimingSensitivity, () -> config.reduceAimingSensitivity, v -> config.reduceAimingSensitivity = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeControlsGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
|
||||||
|
Function<Boolean, Component> holdToggleFormatter = v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold"));
|
||||||
|
|
||||||
|
return OptionGroup.createBuilder()
|
||||||
|
.name(Component.literal("Controls"))
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.toggle_sprint"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.toggle_sprint.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.toggleSprint, () -> config.toggleSprint, v -> config.toggleSprint = v)
|
||||||
|
.controller(opt -> BooleanControllerBuilder.create(opt)
|
||||||
|
.valueFormatter(holdToggleFormatter)
|
||||||
|
.coloured(false))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.toggle_sneak"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.toggle_sneak.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.toggleSneak, () -> config.toggleSneak, v -> config.toggleSneak = v)
|
||||||
|
.controller(opt -> BooleanControllerBuilder.create(opt)
|
||||||
|
.valueFormatter(holdToggleFormatter)
|
||||||
|
.coloured(false))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.auto_jump"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.auto_jump.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.autoJump, () -> config.autoJump, v -> config.autoJump = v)
|
||||||
|
.controller(opt -> BooleanControllerBuilder.create(opt)
|
||||||
|
.onOffFormatter())
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeAccessibilityGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
|
||||||
|
return OptionGroup.createBuilder()
|
||||||
|
.name(Component.literal("Accessibility"))
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.show_ingame_guide"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.show_ingame_guide.tooltip"))
|
||||||
|
.image(screenshot("ingame-button-guide.png"), 961, 306)
|
||||||
|
.build())
|
||||||
|
.binding(def.showIngameGuide, () -> config.showIngameGuide, v -> config.showIngameGuide = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.show_screen_guide"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.show_screen_guide.tooltip"))
|
||||||
|
.webpImage(screenshot("screen-button-guide.webp"))
|
||||||
|
.build())
|
||||||
|
.binding(def.showScreenGuide, () -> config.showScreenGuide, v -> config.showScreenGuide = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.chat_screen_offset"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.chat_screen_offset.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.chatKeyboardHeight, () -> config.chatKeyboardHeight, v -> config.chatKeyboardHeight = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 8f).step(0.1f).valueFormatter(percentFormatter))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeDeadzoneGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
|
||||||
|
var group = OptionGroup.createBuilder()
|
||||||
|
.name(Component.literal("Deadzones"));
|
||||||
|
if (controller instanceof GamepadController gamepad) {
|
||||||
|
var gpCfg = gamepad.config();
|
||||||
|
var gpCfgDef = gamepad.defaultConfig();
|
||||||
|
group
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick")))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick")))
|
||||||
|
.description(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
||||||
|
.build())
|
||||||
|
.binding(
|
||||||
|
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
|
||||||
|
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
|
||||||
|
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
|
||||||
|
)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 1f).step(0.01f)
|
||||||
|
.valueFormatter(percentFormatter))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick")))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick")))
|
||||||
|
.description(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
||||||
|
.build())
|
||||||
|
.binding(
|
||||||
|
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
|
||||||
|
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
|
||||||
|
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
|
||||||
|
)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 1f).step(0.01f)
|
||||||
|
.valueFormatter(percentFormatter))
|
||||||
|
.build());
|
||||||
|
} else if (controller instanceof SingleJoystickController joystick) {
|
||||||
|
JoystickMapping.Axis[] axes = joystick.mapping().axes();
|
||||||
|
Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
|
||||||
|
.filter(i -> axes[i].requiresDeadzone())
|
||||||
|
.boxed()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
i -> axes[i].identifier(),
|
||||||
|
i -> i,
|
||||||
|
(x, y) -> x,
|
||||||
|
LinkedHashMap::new
|
||||||
|
))
|
||||||
|
.values();
|
||||||
|
var jsCfg = joystick.config();
|
||||||
|
var jsCfgDef = joystick.defaultConfig();
|
||||||
|
|
||||||
|
for (int i : deadzoneAxes) {
|
||||||
|
var axis = axes[i];
|
||||||
|
|
||||||
|
group.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
|
||||||
|
.description(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
||||||
|
.build())
|
||||||
|
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 1f).step(0.01f)
|
||||||
|
.valueFormatter(percentFormatter))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.option(Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.button_activation_threshold"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.button_activation_threshold.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.buttonActivationThreshold, () -> config.buttonActivationThreshold, v -> config.buttonActivationThreshold = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 1f).step(0.01f)
|
||||||
|
.valueFormatter(percentFormatter))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
group.option(ButtonOption.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.auto_calibration"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.auto_calibration.tooltip"))
|
||||||
|
.build())
|
||||||
|
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return group.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConfigCategory createAdvancedCategory(Controller<?, ?> controller) {
|
||||||
|
return ConfigCategory.createBuilder()
|
||||||
|
.name(Component.literal("Advanced"))
|
||||||
|
.group(makeVibrationGroup(controller))
|
||||||
|
.group(makeGyroGroup(controller))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConfigCategory createBindsCategory(Controller<?, ?> controller) {
|
||||||
|
var category = ConfigCategory.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.group.controls"));
|
||||||
|
|
||||||
|
List<OptionBindPair> optionBinds = new ArrayList<>();
|
||||||
|
groupBindings(controller.bindings().registry().values()).forEach((categoryName, bindGroup) -> {
|
||||||
|
var controlsGroup = OptionGroup.createBuilder()
|
||||||
|
.name(categoryName);
|
||||||
|
|
||||||
|
controlsGroup.options(bindGroup.stream().map(binding -> {
|
||||||
|
Option.Builder<?> option = binding.startYACLOption()
|
||||||
|
.listener((opt, val) -> updateConflictingBinds(optionBinds));
|
||||||
|
|
||||||
|
Option<?> built = option.build();
|
||||||
|
optionBinds.add(new OptionBindPair(built, binding));
|
||||||
|
return built;
|
||||||
|
}).toList());
|
||||||
|
|
||||||
|
category.group(controlsGroup.build());
|
||||||
|
});
|
||||||
|
updateConflictingBinds(optionBinds);
|
||||||
|
|
||||||
|
return category.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateConflictingBinds(List<OptionBindPair> all) {
|
||||||
|
all.forEach(pair -> ((AbstractBindController<?>) pair.option().controller()).setConflicting(false));
|
||||||
|
|
||||||
|
for (OptionBindPair opt : all) {
|
||||||
|
var ctxs = BindContext.flatten(opt.binding().contexts());
|
||||||
|
|
||||||
|
List<OptionBindPair> conflicting = all.stream()
|
||||||
|
.filter(pair -> pair.binding() != opt.binding())
|
||||||
|
.filter(pair -> {
|
||||||
|
boolean contextsMatch = BindContext.flatten(pair.binding().contexts())
|
||||||
|
.stream()
|
||||||
|
.anyMatch(ctxs::contains);
|
||||||
|
boolean bindMatches = pair.option().pendingValue().equals(opt.option().pendingValue());
|
||||||
|
boolean bindIsNotEmpty = !(pair.option().pendingValue() instanceof EmptyBind<?>);
|
||||||
|
return contextsMatch && bindMatches && bindIsNotEmpty;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
conflicting.forEach(conflict -> ((AbstractBindController<?>) conflict.option().controller()).setConflicting(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeVibrationGroup(Controller<?, ?> controller) {
|
||||||
|
boolean canRumble = controller.supportsRumble();
|
||||||
|
var config = controller.config();
|
||||||
|
var def = controller.defaultConfig();
|
||||||
|
|
||||||
|
var vibrationGroup = OptionGroup.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.group.vibration"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.group.vibration.tooltip"))
|
||||||
|
.build());
|
||||||
|
if (canRumble) {
|
||||||
|
List<Option<Float>> strengthOptions = new ArrayList<>();
|
||||||
|
Option<Boolean> allowVibrationOption;
|
||||||
|
vibrationGroup.option(allowVibrationOption = Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.allow_vibrations"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.allow_vibrations.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(def.allowVibrations, () -> config.allowVibrations, v -> config.allowVibrations = v)
|
||||||
|
.listener((opt, allowVibration) -> strengthOptions.forEach(so -> so.setAvailable(allowVibration)))
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build());
|
||||||
|
for (RumbleSource source : RumbleSource.values()) {
|
||||||
|
var option = Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath()))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath() + ".tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(
|
||||||
|
def.getRumbleStrength(source),
|
||||||
|
() -> config.getRumbleStrength(source),
|
||||||
|
v -> config.setRumbleStrength(source, v)
|
||||||
|
)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 2f)
|
||||||
|
.step(0.05f)
|
||||||
|
.valueFormatter(percentOrOffFormatter))
|
||||||
|
.available(allowVibrationOption.pendingValue())
|
||||||
|
.build();
|
||||||
|
strengthOptions.add(option);
|
||||||
|
vibrationGroup.option(option);
|
||||||
|
}
|
||||||
|
vibrationGroup.option(ButtonOption.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.test_vibration"))
|
||||||
|
.description(OptionDescription.of(Component.translatable("controlify.gui.test_vibration.tooltip")))
|
||||||
|
.action((screen, btn) -> {
|
||||||
|
controller.rumbleManager().play(
|
||||||
|
RumbleSource.MASTER,
|
||||||
|
BasicRumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
|
||||||
|
.join(BasicRumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
|
||||||
|
.repeat(3)
|
||||||
|
.join(BasicRumbleEffect.constant(1f, 0f, 5)
|
||||||
|
.join(BasicRumbleEffect.constant(0f, 1f, 5))
|
||||||
|
.repeat(10)
|
||||||
|
)
|
||||||
|
.earlyFinish(BasicRumbleEffect.finishOnScreenChange())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
vibrationGroup.option(LabelOption.create(Component.translatable("controlify.gui.allow_vibrations.not_available").withStyle(ChatFormatting.RED)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return vibrationGroup.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionGroup makeGyroGroup(Controller<?, ?> controller) {
|
||||||
|
GamepadController gamepad = (controller instanceof GamepadController) ? (GamepadController) controller : null;
|
||||||
|
boolean hasGyro = gamepad != null && gamepad.hasGyro();
|
||||||
|
|
||||||
|
var gpCfg = gamepad != null ? gamepad.config() : null;
|
||||||
|
var gpCfgDef = gamepad != null ? gamepad.defaultConfig() : null;
|
||||||
|
|
||||||
|
Option<Float> gyroSensitivity;
|
||||||
|
List<Option<?>> gyroOptions = new ArrayList<>();
|
||||||
|
var gyroGroup = OptionGroup.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.group.gyro"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.group.gyro.tooltip"))
|
||||||
|
.build())
|
||||||
|
.collapsed(!hasGyro);
|
||||||
|
if (hasGyro) {
|
||||||
|
gyroGroup.option(gyroSensitivity = Option.<Float>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.gyro_look_sensitivity"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(gpCfgDef.gyroLookSensitivity, () -> gpCfg.gyroLookSensitivity, v -> gpCfg.gyroLookSensitivity = v)
|
||||||
|
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||||
|
.range(0f, 1f)
|
||||||
|
.step(0.05f)
|
||||||
|
.valueFormatter(percentOrOffFormatter))
|
||||||
|
.listener((opt, sensitivity) -> gyroOptions.forEach(o -> {
|
||||||
|
o.setAvailable(sensitivity > 0);
|
||||||
|
o.requestSetDefault();
|
||||||
|
}))
|
||||||
|
.build());
|
||||||
|
gyroGroup.option(Util.make(() -> {
|
||||||
|
var opt = Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.gyro_requires_button"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.gyro_requires_button.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.available(gyroSensitivity.pendingValue() > 0)
|
||||||
|
.build();
|
||||||
|
gyroOptions.add(opt);
|
||||||
|
return opt;
|
||||||
|
}));
|
||||||
|
gyroGroup.option(Util.make(() -> {
|
||||||
|
var opt = Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.flick_stick"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.flick_stick.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(gpCfgDef.flickStick, () -> gpCfg.flickStick, v -> gpCfg.flickStick = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.available(gyroSensitivity.pendingValue() > 0)
|
||||||
|
.build();
|
||||||
|
gyroOptions.add(opt);
|
||||||
|
return opt;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
gyroGroup.option(LabelOption.create(Component.translatable("controlify.gui.group.gyro.no_gyro.tooltip").withStyle(ChatFormatting.RED)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return gyroGroup.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Component, List<ControllerBinding>> groupBindings(Collection<ControllerBinding> bindings) {
|
||||||
|
return bindings.stream()
|
||||||
|
.collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceLocation screenshot(String filename) {
|
||||||
|
return Controlify.id("textures/screenshots/" + filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record OptionBindPair(Option<?> option, ControllerBinding binding) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package dev.isxander.controlify.config.gui;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.config.GlobalSettings;
|
||||||
|
import dev.isxander.controlify.reacharound.ReachAroundMode;
|
||||||
|
import dev.isxander.yacl.api.*;
|
||||||
|
import dev.isxander.yacl.api.controller.BooleanControllerBuilder;
|
||||||
|
import dev.isxander.yacl.api.controller.EnumControllerBuilder;
|
||||||
|
import dev.isxander.yacl.api.controller.TickBoxControllerBuilder;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.Util;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
public class GlobalSettingsGui {
|
||||||
|
public static Screen createGlobalSettingsScreen(Screen parent) {
|
||||||
|
var globalSettings = Controlify.instance().config().globalSettings();
|
||||||
|
return YetAnotherConfigLib.createBuilder()
|
||||||
|
.title(Component.literal("Controlify Global Settings"))
|
||||||
|
.category(ConfigCategory.createBuilder()
|
||||||
|
.name(Component.literal("Global Settings"))
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.load_vibration_natives"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.load_vibration_natives.tooltip"))
|
||||||
|
.description(Component.translatable("controlify.gui.load_vibration_natives.tooltip.warning").withStyle(ChatFormatting.RED))
|
||||||
|
.build())
|
||||||
|
.binding(true, () -> globalSettings.loadVibrationNatives, v -> globalSettings.loadVibrationNatives = v)
|
||||||
|
.controller(opt -> BooleanControllerBuilder.create(opt).yesNoFormatter())
|
||||||
|
.flag(OptionFlag.GAME_RESTART)
|
||||||
|
.build())
|
||||||
|
.option(Option.<ReachAroundMode>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.reach_around"))
|
||||||
|
.description(state -> OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.reach_around.tooltip"))
|
||||||
|
.description(Component.translatable("controlify.gui.reach_around.tooltip.parity").withStyle(ChatFormatting.GRAY))
|
||||||
|
.description(state == ReachAroundMode.EVERYWHERE ? Component.translatable("controlify.gui.reach_around.tooltip.warning").withStyle(ChatFormatting.RED) : Component.empty())
|
||||||
|
.build())
|
||||||
|
.binding(GlobalSettings.DEFAULT.reachAround, () -> globalSettings.reachAround, v -> globalSettings.reachAround = v)
|
||||||
|
.controller(opt -> EnumControllerBuilder.create(opt).enumClass(ReachAroundMode.class))
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.ui_sounds"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.ui_sounds.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.notify_low_battery"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.notify_low_battery.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(GlobalSettings.DEFAULT.notifyLowBattery, () -> globalSettings.notifyLowBattery, v -> globalSettings.notifyLowBattery = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.out_of_focus_input"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.out_of_focus_input.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(GlobalSettings.DEFAULT.outOfFocusInput, () -> globalSettings.outOfFocusInput, v -> globalSettings.outOfFocusInput = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(Option.<Boolean>createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.keyboard_movement"))
|
||||||
|
.description(OptionDescription.createBuilder()
|
||||||
|
.description(Component.translatable("controlify.gui.keyboard_movement.tooltip"))
|
||||||
|
.build())
|
||||||
|
.binding(GlobalSettings.DEFAULT.keyboardMovement, () -> globalSettings.keyboardMovement, v -> globalSettings.keyboardMovement = v)
|
||||||
|
.controller(TickBoxControllerBuilder::create)
|
||||||
|
.build())
|
||||||
|
.option(ButtonOption.createBuilder()
|
||||||
|
.name(Component.translatable("controlify.gui.open_issue_tracker"))
|
||||||
|
.action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues"))
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.build().generateScreen(parent);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,6 @@ import com.terraformersmc.modmenu.api.ModMenuApi;
|
|||||||
public class ModMenuIntegration implements ModMenuApi {
|
public class ModMenuIntegration implements ModMenuApi {
|
||||||
@Override
|
@Override
|
||||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||||
return YACLHelper::openConfigScreen;
|
return ControllerCarouselScreen::createConfigScreen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,500 +0,0 @@
|
|||||||
package dev.isxander.controlify.config.gui;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import dev.isxander.controlify.Controlify;
|
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.api.bind.ControllerBinding;
|
|
||||||
import dev.isxander.controlify.bindings.BindContext;
|
|
||||||
import dev.isxander.controlify.bindings.EmptyBind;
|
|
||||||
import dev.isxander.controlify.config.GlobalSettings;
|
|
||||||
import dev.isxander.controlify.controller.BatteryLevel;
|
|
||||||
import dev.isxander.controlify.controller.Controller;
|
|
||||||
import dev.isxander.controlify.controller.ControllerConfig;
|
|
||||||
import dev.isxander.controlify.controller.ControllerState;
|
|
||||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
|
||||||
import dev.isxander.controlify.controller.gamepad.BuiltinGamepadTheme;
|
|
||||||
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
|
||||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
|
||||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
|
||||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
|
||||||
import dev.isxander.controlify.gui.screen.SDLOnboardingScreen;
|
|
||||||
import dev.isxander.controlify.reacharound.ReachAroundMode;
|
|
||||||
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
|
||||||
import dev.isxander.controlify.rumble.RumbleSource;
|
|
||||||
import dev.isxander.controlify.rumble.RumbleState;
|
|
||||||
import dev.isxander.yacl.api.*;
|
|
||||||
import dev.isxander.yacl.gui.controllers.ActionController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.BooleanController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.TickBoxController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.cycling.CyclingListController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.cycling.EnumController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.slider.FloatSliderController;
|
|
||||||
import dev.isxander.yacl.gui.controllers.string.StringController;
|
|
||||||
import net.minecraft.ChatFormatting;
|
|
||||||
import net.minecraft.Util;
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
|
||||||
import net.minecraft.network.chat.CommonComponents;
|
|
||||||
import net.minecraft.network.chat.Component;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class YACLHelper {
|
|
||||||
private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
|
|
||||||
private static final Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v);
|
|
||||||
|
|
||||||
public static Screen openConfigScreen(Screen parent) {
|
|
||||||
var controlify = Controlify.instance();
|
|
||||||
|
|
||||||
if (!controlify.config().globalSettings().vibrationOnboarded) {
|
|
||||||
return new SDLOnboardingScreen(() -> generateConfigScreen(parent), yes -> {
|
|
||||||
if (yes) {
|
|
||||||
SDL2NativesManager.initialise();
|
|
||||||
|
|
||||||
if (controlify.config().globalSettings().delegateSetup) {
|
|
||||||
controlify.discoverControllers();
|
|
||||||
controlify.config().globalSettings().delegateSetup = false;
|
|
||||||
controlify.config().save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (Controlify.instance().config().globalSettings().delegateSetup) {
|
|
||||||
controlify.discoverControllers();
|
|
||||||
controlify.config().globalSettings().delegateSetup = false;
|
|
||||||
controlify.config().save();
|
|
||||||
}
|
|
||||||
return generateConfigScreen(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Screen generateConfigScreen(Screen parent) {
|
|
||||||
var controlify = Controlify.instance();
|
|
||||||
|
|
||||||
var yacl = YetAnotherConfigLib.createBuilder()
|
|
||||||
.title(Component.literal("Controlify"))
|
|
||||||
.save(() -> controlify.config().save());
|
|
||||||
|
|
||||||
Option<Boolean> globalVibrationOption;
|
|
||||||
|
|
||||||
var globalSettings = Controlify.instance().config().globalSettings();
|
|
||||||
var globalCategory = ConfigCategory.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.category.global"))
|
|
||||||
.option(Option.createBuilder((Class<Controller<?, ?>>) (Class<?>) Controller.class)
|
|
||||||
.name(Component.translatable("controlify.gui.current_controller"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.current_controller.tooltip"))
|
|
||||||
.binding(Controlify.instance().getCurrentController().orElse(Controller.DUMMY), () -> Controlify.instance().getCurrentController().orElse(Controller.DUMMY), v -> Controlify.instance().setCurrentController(v))
|
|
||||||
.controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), ControllerManager.getConnectedControllers().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name())))
|
|
||||||
.build())
|
|
||||||
.option(globalVibrationOption = Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.load_vibration_natives"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.load_vibration_natives.tooltip"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.load_vibration_natives.tooltip.warning").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(true, () -> globalSettings.loadVibrationNatives, v -> globalSettings.loadVibrationNatives = v)
|
|
||||||
.controller(opt -> new BooleanController(opt, BooleanController.YES_NO_FORMATTER, false))
|
|
||||||
.flag(OptionFlag.GAME_RESTART)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(ReachAroundMode.class)
|
|
||||||
.name(Component.translatable("controlify.gui.reach_around"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.reach_around.tooltip"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.reach_around.tooltip.parity").withStyle(ChatFormatting.GRAY))
|
|
||||||
.tooltip(state -> state == ReachAroundMode.EVERYWHERE ? Component.translatable("controlify.gui.reach_around.tooltip.warning").withStyle(ChatFormatting.RED) : Component.empty())
|
|
||||||
.binding(GlobalSettings.DEFAULT.reachAround, () -> globalSettings.reachAround, v -> globalSettings.reachAround = v)
|
|
||||||
.controller(EnumController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.ui_sounds"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.ui_sounds.tooltip"))
|
|
||||||
.binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.notify_low_battery"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.notify_low_battery.tooltip"))
|
|
||||||
.binding(GlobalSettings.DEFAULT.notifyLowBattery, () -> globalSettings.notifyLowBattery, v -> globalSettings.notifyLowBattery = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.out_of_focus_input"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.out_of_focus_input.tooltip"))
|
|
||||||
.binding(GlobalSettings.DEFAULT.outOfFocusInput, () -> globalSettings.outOfFocusInput, v -> globalSettings.outOfFocusInput = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.keyboard_movement"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.keyboard_movement.tooltip"))
|
|
||||||
.binding(GlobalSettings.DEFAULT.keyboardMovement, () -> globalSettings.keyboardMovement, v -> globalSettings.keyboardMovement = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build())
|
|
||||||
.option(ButtonOption.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.open_issue_tracker"))
|
|
||||||
.action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues"))
|
|
||||||
.controller(opt -> new ActionController(opt, Component.translatable("controlify.gui.format.open")))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
yacl.category(globalCategory.build());
|
|
||||||
|
|
||||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
|
||||||
yacl.category(createControllerCategory(controller, globalVibrationOption));
|
|
||||||
}
|
|
||||||
|
|
||||||
return yacl.build().generateScreen(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <S extends ControllerState, C extends ControllerConfig> ConfigCategory createControllerCategory(Controller<S, C> controller, Option<Boolean> globalVibrationOption) {
|
|
||||||
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();
|
|
||||||
|
|
||||||
category.name(Component.literal(controller.name()));
|
|
||||||
|
|
||||||
if (controller.batteryLevel() != BatteryLevel.UNKNOWN) {
|
|
||||||
category.option(LabelOption.create(Component.translatable("controlify.gui.battery_level", controller.batteryLevel().getFriendlyName())));
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = controller.config();
|
|
||||||
var def = controller.defaultConfig();
|
|
||||||
|
|
||||||
var basicGroup = OptionGroup.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.group.basic"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.group.basic.tooltip"))
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.horizontal_look_sensitivity"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.horizontal_look_sensitivity.tooltip"))
|
|
||||||
.binding(def.horizontalLookSensitivity, () -> config.horizontalLookSensitivity, v -> config.horizontalLookSensitivity = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 0.05f, percentFormatter))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.vertical_look_sensitivity"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.vertical_look_sensitivity.tooltip"))
|
|
||||||
.binding(def.verticalLookSensitivity, () -> config.verticalLookSensitivity, v -> config.verticalLookSensitivity = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 0.05f, percentFormatter))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.toggle_sprint"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.toggle_sprint.tooltip"))
|
|
||||||
.binding(def.toggleSprint, () -> config.toggleSprint, v -> config.toggleSprint = v)
|
|
||||||
.controller(opt -> new BooleanController(opt, v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold")), false))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.toggle_sneak"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.toggle_sneak.tooltip"))
|
|
||||||
.binding(def.toggleSneak, () -> config.toggleSneak, v -> config.toggleSneak = v)
|
|
||||||
.controller(opt -> new BooleanController(opt, v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold")), false))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.auto_jump"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.auto_jump.tooltip"))
|
|
||||||
.binding(def.autoJump, () -> config.autoJump, v -> config.autoJump = v)
|
|
||||||
.controller(BooleanController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.show_ingame_guide"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.show_ingame_guide.tooltip"))
|
|
||||||
.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)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.vmouse_sensitivity"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.vmouse_sensitivity.tooltip"))
|
|
||||||
.binding(def.virtualMouseSensitivity, () -> config.virtualMouseSensitivity, v -> config.virtualMouseSensitivity = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0.1f, 2f, 0.05f, percentFormatter))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.chat_screen_offset"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.chat_screen_offset.tooltip"))
|
|
||||||
.binding(def.chatKeyboardHeight, () -> config.chatKeyboardHeight, v -> config.chatKeyboardHeight = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0f, 0.8f, 0.1f, percentFormatter))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.reduce_aiming_sensitivity"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.reduce_aiming_sensitivity.tooltip"))
|
|
||||||
.binding(def.reduceAimingSensitivity, () -> config.reduceAimingSensitivity, v -> config.reduceAimingSensitivity = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
if (controller instanceof GamepadController gamepad) {
|
|
||||||
var gamepadConfig = gamepad.config();
|
|
||||||
var defaultGamepadConfig = gamepad.defaultConfig();
|
|
||||||
|
|
||||||
basicGroup.option(Option.createBuilder(BuiltinGamepadTheme.class)
|
|
||||||
.name(Component.translatable("controlify.gui.controller_theme"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.controller_theme.tooltip"))
|
|
||||||
.binding(defaultGamepadConfig.theme, () -> gamepadConfig.theme, v -> gamepadConfig.theme = v)
|
|
||||||
.controller(EnumController::new)
|
|
||||||
.instant(true)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
basicGroup
|
|
||||||
.option(Option.createBuilder(String.class)
|
|
||||||
.name(Component.translatable("controlify.gui.custom_name"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.custom_name.tooltip"))
|
|
||||||
.binding(def.customName == null ? "" : def.customName, () -> config.customName == null ? "" : config.customName, v -> config.customName = (v.equals("") ? null : v))
|
|
||||||
.controller(StringController::new)
|
|
||||||
.build());
|
|
||||||
category.group(basicGroup.build());
|
|
||||||
|
|
||||||
category.group(makeVibrationGroup(globalVibrationOption, controller));
|
|
||||||
|
|
||||||
category.group(makeGyroGroup(controller));
|
|
||||||
|
|
||||||
var advancedGroup = OptionGroup.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.group.advanced"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.group.advanced.tooltip"))
|
|
||||||
.collapsed(true);
|
|
||||||
|
|
||||||
addDeadzoneOptions(controller, advancedGroup);
|
|
||||||
|
|
||||||
advancedGroup
|
|
||||||
.option(ButtonOption.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.auto_calibration"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.auto_calibration.tooltip"))
|
|
||||||
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen)))
|
|
||||||
.controller(ActionController::new)
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.button_activation_threshold"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.button_activation_threshold.tooltip"))
|
|
||||||
.binding(def.buttonActivationThreshold, () -> config.buttonActivationThreshold, v -> config.buttonActivationThreshold = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.05f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
category.group(advancedGroup.build());
|
|
||||||
|
|
||||||
var controlsGroup = OptionGroup.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.group.controls"));
|
|
||||||
List<OptionBindPair> optionBinds = new ArrayList<>();
|
|
||||||
groupBindings(controller.bindings().registry().values()).forEach((categoryName, bindGroup) -> {
|
|
||||||
controlsGroup.option(LabelOption.create(categoryName));
|
|
||||||
controlsGroup.options(bindGroup.stream().map(binding -> {
|
|
||||||
Option.Builder<?> option = binding.startYACLOption()
|
|
||||||
.listener((opt, val) -> updateConflictingBinds(optionBinds));
|
|
||||||
|
|
||||||
Option<?> built = option.build();
|
|
||||||
optionBinds.add(new OptionBindPair(built, binding));
|
|
||||||
return built;
|
|
||||||
}).toList());
|
|
||||||
});
|
|
||||||
updateConflictingBinds(optionBinds);
|
|
||||||
|
|
||||||
category.group(controlsGroup.build());
|
|
||||||
|
|
||||||
return category.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateConflictingBinds(List<OptionBindPair> all) {
|
|
||||||
all.forEach(pair -> ((AbstractBindController<?>) pair.option().controller()).setConflicting(false));
|
|
||||||
|
|
||||||
for (OptionBindPair opt : all) {
|
|
||||||
var ctxs = BindContext.flatten(opt.binding().contexts());
|
|
||||||
|
|
||||||
List<OptionBindPair> conflicting = all.stream()
|
|
||||||
.filter(pair -> pair.binding() != opt.binding())
|
|
||||||
.filter(pair -> {
|
|
||||||
boolean contextsMatch = BindContext.flatten(pair.binding().contexts())
|
|
||||||
.stream()
|
|
||||||
.anyMatch(ctxs::contains);
|
|
||||||
boolean bindMatches = pair.option().pendingValue().equals(opt.option().pendingValue());
|
|
||||||
boolean bindIsNotEmpty = !(pair.option().pendingValue() instanceof EmptyBind<?>);
|
|
||||||
return contextsMatch && bindMatches && bindIsNotEmpty;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
conflicting.forEach(conflict -> ((AbstractBindController<?>) conflict.option().controller()).setConflicting(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OptionGroup makeVibrationGroup(Option<Boolean> globalVibrationOption, Controller<?, ?> controller) {
|
|
||||||
boolean canRumble = controller.supportsRumble();
|
|
||||||
var config = controller.config();
|
|
||||||
var def = controller.defaultConfig();
|
|
||||||
|
|
||||||
var vibrationGroup = OptionGroup.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.group.vibration"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.group.vibration.tooltip"))
|
|
||||||
.tooltip(canRumble ? Component.empty() : Component.translatable("controlify.gui.allow_vibrations.not_available").withStyle(ChatFormatting.RED))
|
|
||||||
.collapsed(!canRumble);
|
|
||||||
List<Option<Float>> strengthOptions = new ArrayList<>();
|
|
||||||
Option<Boolean> allowVibrationOption;
|
|
||||||
vibrationGroup.option(allowVibrationOption = Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.allow_vibrations"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.allow_vibrations.tooltip"))
|
|
||||||
.tooltip(canRumble ? Component.empty() : Component.translatable("controlify.gui.allow_vibrations.not_available").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(globalVibrationOption.pendingValue(), () -> config.allowVibrations && globalVibrationOption.pendingValue(), v -> config.allowVibrations = v)
|
|
||||||
.available(globalVibrationOption.pendingValue() && canRumble)
|
|
||||||
.listener((opt, allowVibration) -> strengthOptions.forEach(so -> so.setAvailable(allowVibration)))
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.build());
|
|
||||||
for (RumbleSource source : RumbleSource.values()) {
|
|
||||||
var option = Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath()))
|
|
||||||
.tooltip(Component.translatable("controlify.vibration_strength." + source.id().getNamespace() + "." + source.id().getPath() + ".tooltip"))
|
|
||||||
.tooltip(canRumble ? Component.empty() : Component.translatable("controlify.gui.allow_vibrations.not_available").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(
|
|
||||||
def.getRumbleStrength(source),
|
|
||||||
() -> config.getRumbleStrength(source),
|
|
||||||
v -> config.setRumbleStrength(source, v)
|
|
||||||
)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0f, 2f, 0.05f, percentOrOffFormatter))
|
|
||||||
.available(allowVibrationOption.pendingValue() && canRumble)
|
|
||||||
.build();
|
|
||||||
strengthOptions.add(option);
|
|
||||||
vibrationGroup.option(option);
|
|
||||||
}
|
|
||||||
vibrationGroup.option(ButtonOption.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.test_vibration"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.test_vibration.tooltip"))
|
|
||||||
.controller(ActionController::new)
|
|
||||||
.action((screen, btn) -> {
|
|
||||||
controller.rumbleManager().play(
|
|
||||||
RumbleSource.MASTER,
|
|
||||||
BasicRumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
|
|
||||||
.join(BasicRumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
|
|
||||||
.repeat(3)
|
|
||||||
.join(BasicRumbleEffect.constant(1f, 0f, 5)
|
|
||||||
.join(BasicRumbleEffect.constant(0f, 1f, 5))
|
|
||||||
.repeat(10)
|
|
||||||
)
|
|
||||||
.earlyFinish(BasicRumbleEffect.finishOnScreenChange())
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.build());
|
|
||||||
|
|
||||||
return vibrationGroup.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OptionGroup makeGyroGroup(Controller<?, ?> controller) {
|
|
||||||
GamepadController gamepad = (controller instanceof GamepadController) ? (GamepadController) controller : null;
|
|
||||||
boolean hasGyro = gamepad != null && gamepad.hasGyro();
|
|
||||||
|
|
||||||
var gpCfg = gamepad != null ? gamepad.config() : null;
|
|
||||||
var gpCfgDef = gamepad != null ? gamepad.defaultConfig() : null;
|
|
||||||
|
|
||||||
Component noGyroTooltip = Component.translatable("controlify.gui.group.gyro.no_gyro.tooltip").withStyle(ChatFormatting.RED);
|
|
||||||
|
|
||||||
Option<Float> gyroSensitivity;
|
|
||||||
List<Option<?>> gyroOptions = new ArrayList<>();
|
|
||||||
var gyroGroup = OptionGroup.createBuilder()
|
|
||||||
.name(Component.translatable("controlify.gui.group.gyro"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.group.gyro.tooltip"))
|
|
||||||
.tooltip(hasGyro ? Component.empty() : noGyroTooltip)
|
|
||||||
.collapsed(!hasGyro)
|
|
||||||
.option(gyroSensitivity = Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.gyro_look_sensitivity"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.gyro_look_sensitivity.tooltip"))
|
|
||||||
.tooltip(hasGyro ? Component.empty() : noGyroTooltip)
|
|
||||||
.available(hasGyro)
|
|
||||||
.binding(hasGyro ? gpCfgDef.gyroLookSensitivity : 0, () -> hasGyro ? gpCfg.gyroLookSensitivity : 0, v -> gpCfg.gyroLookSensitivity = v)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0f, 1f, 0.05f, percentOrOffFormatter))
|
|
||||||
.listener((opt, sensitivity) -> gyroOptions.forEach(o -> {
|
|
||||||
o.setAvailable(sensitivity > 0);
|
|
||||||
o.requestSetDefault();
|
|
||||||
}))
|
|
||||||
.build())
|
|
||||||
.option(Util.make(() -> {
|
|
||||||
var opt = Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.gyro_requires_button"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.gyro_requires_button.tooltip"))
|
|
||||||
.tooltip(hasGyro ? Component.empty() : noGyroTooltip)
|
|
||||||
.available(hasGyro)
|
|
||||||
.binding(hasGyro ? gpCfgDef.gyroRequiresButton : false, () -> hasGyro ? gpCfg.gyroRequiresButton : false, v -> gpCfg.gyroRequiresButton = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.available(gyroSensitivity.pendingValue() > 0)
|
|
||||||
.build();
|
|
||||||
gyroOptions.add(opt);
|
|
||||||
return opt;
|
|
||||||
}))
|
|
||||||
.option(Util.make(() -> {
|
|
||||||
var opt = Option.createBuilder(boolean.class)
|
|
||||||
.name(Component.translatable("controlify.gui.flick_stick"))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.flick_stick.tooltip"))
|
|
||||||
.tooltip(hasGyro ? Component.empty() : noGyroTooltip)
|
|
||||||
.available(hasGyro)
|
|
||||||
.binding(hasGyro ? gpCfgDef.flickStick : false, () -> hasGyro ? gpCfg.flickStick : false, v -> gpCfg.flickStick = v)
|
|
||||||
.controller(TickBoxController::new)
|
|
||||||
.available(gyroSensitivity.pendingValue() > 0)
|
|
||||||
.build();
|
|
||||||
gyroOptions.add(opt);
|
|
||||||
return opt;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return gyroGroup.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addDeadzoneOptions(Controller<?, ?> controller, OptionGroup.Builder group) {
|
|
||||||
if (controller instanceof GamepadController gamepad) {
|
|
||||||
var gpCfg = gamepad.config();
|
|
||||||
var gpCfgDef = gamepad.defaultConfig();
|
|
||||||
group
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick")))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick")))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(
|
|
||||||
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
|
|
||||||
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
|
|
||||||
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
|
|
||||||
)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
|
||||||
.build())
|
|
||||||
.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick")))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick")))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(
|
|
||||||
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
|
|
||||||
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY),
|
|
||||||
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
|
|
||||||
)
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
|
||||||
.build());
|
|
||||||
} else if (controller instanceof SingleJoystickController joystick) {
|
|
||||||
JoystickMapping.Axis[] axes = joystick.mapping().axes();
|
|
||||||
Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
|
|
||||||
.filter(i -> axes[i].requiresDeadzone())
|
|
||||||
.boxed()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
i -> axes[i].identifier(),
|
|
||||||
i -> i,
|
|
||||||
(x, y) -> x,
|
|
||||||
LinkedHashMap::new
|
|
||||||
))
|
|
||||||
.values();
|
|
||||||
var jsCfg = joystick.config();
|
|
||||||
var jsCfgDef = joystick.defaultConfig();
|
|
||||||
|
|
||||||
for (int i : deadzoneAxes) {
|
|
||||||
var axis = axes[i];
|
|
||||||
|
|
||||||
group.option(Option.createBuilder(float.class)
|
|
||||||
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
|
|
||||||
.tooltip(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
|
|
||||||
.binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v))
|
|
||||||
.controller(opt -> new FloatSliderController(opt, 0, 1, 0.01f, v -> Component.literal(String.format("%.0f%%", v*100))))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<Component, List<ControllerBinding>> groupBindings(Collection<ControllerBinding> bindings) {
|
|
||||||
return bindings.stream()
|
|
||||||
.collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private record OptionBindPair(Option<?> option, ControllerBinding binding) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,8 @@ import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
|||||||
import dev.isxander.controlify.rumble.RumbleCapable;
|
import dev.isxander.controlify.rumble.RumbleCapable;
|
||||||
import dev.isxander.controlify.rumble.RumbleManager;
|
import dev.isxander.controlify.rumble.RumbleManager;
|
||||||
import dev.isxander.controlify.rumble.RumbleSource;
|
import dev.isxander.controlify.rumble.RumbleSource;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
|
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
|
||||||
@ -41,6 +43,8 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
|||||||
|
|
||||||
Optional<ControllerHIDService.ControllerHIDInfo> hidInfo();
|
Optional<ControllerHIDService.ControllerHIDInfo> hidInfo();
|
||||||
|
|
||||||
|
ResourceLocation icon();
|
||||||
|
|
||||||
default boolean canBeUsed() {
|
default boolean canBeUsed() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -116,6 +120,11 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation icon() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
return "DUMMY";
|
return "DUMMY";
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package dev.isxander.controlify.controller.gamepad;
|
package dev.isxander.controlify.controller.gamepad;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||||
import dev.isxander.controlify.controller.AbstractController;
|
import dev.isxander.controlify.controller.AbstractController;
|
||||||
import dev.isxander.controlify.controller.BatteryLevel;
|
import dev.isxander.controlify.controller.BatteryLevel;
|
||||||
@ -7,6 +8,7 @@ import dev.isxander.controlify.controller.hid.ControllerHIDService;
|
|||||||
import dev.isxander.controlify.driver.*;
|
import dev.isxander.controlify.driver.*;
|
||||||
import dev.isxander.controlify.rumble.RumbleManager;
|
import dev.isxander.controlify.rumble.RumbleManager;
|
||||||
import dev.isxander.controlify.rumble.RumbleSource;
|
import dev.isxander.controlify.rumble.RumbleSource;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -113,4 +115,10 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
|||||||
public void close() {
|
public void close() {
|
||||||
uniqueDrivers.forEach(Driver::close);
|
uniqueDrivers.forEach(Driver::close);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation icon() {
|
||||||
|
String theme = config().theme == BuiltinGamepadTheme.DEFAULT ? type().themeId() : config().theme.id();
|
||||||
|
return Controlify.id("textures/gui/gamepad/" + theme + "/icon.png");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
package dev.isxander.controlify.controller.joystick;
|
package dev.isxander.controlify.controller.joystick;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.joystick.JoystickConfig;
|
import dev.isxander.controlify.controller.joystick.JoystickConfig;
|
||||||
import dev.isxander.controlify.controller.joystick.JoystickState;
|
import dev.isxander.controlify.controller.joystick.JoystickState;
|
||||||
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 net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
public interface JoystickController<T extends JoystickConfig> extends Controller<JoystickState, T> {
|
public interface JoystickController<T extends JoystickConfig> extends Controller<JoystickState, T> {
|
||||||
JoystickMapping mapping();
|
JoystickMapping mapping();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default ResourceLocation icon() {
|
||||||
|
return Controlify.id("textures/gui/joystick/icon.png");
|
||||||
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
int axisCount();
|
int axisCount();
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.settingsbutton;
|
package dev.isxander.controlify.mixins.feature.settingsbutton;
|
||||||
|
|
||||||
import com.llamalad7.mixinextras.sugar.Local;
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
import dev.isxander.controlify.config.gui.YACLHelper;
|
import dev.isxander.controlify.config.gui.ControllerCarouselScreen;
|
||||||
import net.minecraft.client.Options;
|
import net.minecraft.client.Options;
|
||||||
import net.minecraft.client.gui.components.Button;
|
import net.minecraft.client.gui.components.Button;
|
||||||
import net.minecraft.client.gui.screens.OptionsSubScreen;
|
import net.minecraft.client.gui.screens.OptionsSubScreen;
|
||||||
@ -22,7 +22,7 @@ public class ControlsScreenMixin extends OptionsSubScreen {
|
|||||||
|
|
||||||
@Inject(method = "init", at = @At("RETURN"))
|
@Inject(method = "init", at = @At("RETURN"))
|
||||||
private void addControllerSettings(CallbackInfo ci, @Local(ordinal = 0) int leftX, @Local(ordinal = 1) int rightX, @Local(ordinal = 2) int currentY) {
|
private void addControllerSettings(CallbackInfo ci, @Local(ordinal = 0) int leftX, @Local(ordinal = 1) int rightX, @Local(ordinal = 2) int currentY) {
|
||||||
addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> minecraft.setScreen(YACLHelper.openConfigScreen(this)))
|
addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(this)))
|
||||||
.pos(leftX, currentY)
|
.pos(leftX, currentY)
|
||||||
.width(150)
|
.width(150)
|
||||||
.build());
|
.build());
|
||||||
|
@ -195,6 +195,8 @@ public class AbstractContainerScreenProcessor<T extends AbstractContainerScreen<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setRenderGuide(boolean render) {
|
private void setRenderGuide(boolean render) {
|
||||||
|
render &= ControlifyApi.get().getCurrentController().map(c -> c.config().showScreenGuide).orElse(false);
|
||||||
|
|
||||||
List<Renderable> renderables = ((ScreenAccessor) screen).getRenderables();
|
List<Renderable> renderables = ((ScreenAccessor) screen).getRenderables();
|
||||||
|
|
||||||
if (leftLayout == null || rightLayout == null)
|
if (leftLayout == null || rightLayout == null)
|
||||||
|
53
src/main/java/dev/isxander/controlify/utils/Animator.java
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package dev.isxander.controlify.utils;
|
||||||
|
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
public class Animator {
|
||||||
|
private final List<AnimationConsumer> animations;
|
||||||
|
private final UnaryOperator<Float> easingFunction;
|
||||||
|
private final int durationTicks;
|
||||||
|
private float time;
|
||||||
|
|
||||||
|
public Animator(int durationTicks, UnaryOperator<Float> easingFunction) {
|
||||||
|
this.animations = new ArrayList<>();
|
||||||
|
this.easingFunction = easingFunction;
|
||||||
|
this.durationTicks = durationTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addConsumer(Consumer<Float> consumer, float start, float end) {
|
||||||
|
animations.add(new AnimationConsumer(consumer, start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addConsumer(Consumer<Integer> consumer, int start, int end) {
|
||||||
|
animations.add(new AnimationConsumer(aFloat -> consumer.accept(aFloat.intValue()), start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick(float deltaTime) {
|
||||||
|
time += deltaTime;
|
||||||
|
if (time > durationTicks) {
|
||||||
|
time = durationTicks;
|
||||||
|
}
|
||||||
|
updateConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConsumers() {
|
||||||
|
animations.forEach(consumer -> {
|
||||||
|
float progress = easingFunction.apply(time / durationTicks);
|
||||||
|
float value = Mth.lerp(progress, consumer.start, consumer.end);
|
||||||
|
consumer.consumer.accept(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDone() {
|
||||||
|
return time >= durationTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AnimationConsumer(Consumer<Float> consumer, float start, float end) {
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 960 B |
After Width: | Height: | Size: 702 B |
After Width: | Height: | Size: 993 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 222 KiB |
@ -34,7 +34,7 @@
|
|||||||
"fabricloader": ">=0.14.0",
|
"fabricloader": ">=0.14.0",
|
||||||
"minecraft": ">1.20-",
|
"minecraft": ">1.20-",
|
||||||
"java": ">=17",
|
"java": ">=17",
|
||||||
"yet_another_config_lib": "^2.4.0"
|
"yet_another_config_lib": ">=3.0.0-"
|
||||||
},
|
},
|
||||||
"breaks": {
|
"breaks": {
|
||||||
"midnightcontrols": "*"
|
"midnightcontrols": "*"
|
||||||
|