diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e76e38..3a4186b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,12 +8,12 @@ machete = "2.+" grgit = "5.0.+" blossom = "1.3.+" -minecraft = "1.20-pre2" +minecraft = "1.20-pre6" quilt_mappings = "1" fabric_loader = "0.14.19" fabric_api = "0.81.2+1.20" 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" hid4java = "0.7.0" quilt_json5 = "1.0.3" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index f9460f2..c9c967e 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -5,6 +5,7 @@ import com.mojang.logging.LogUtils; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint; 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.ControllerState; 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.wireless.LowBatteryNotifier; 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.minecraft.CrashReport; import net.minecraft.CrashReportCategory; @@ -335,6 +334,10 @@ public class Controlify implements ControlifyApi { } canDiscoverControllers = false; + + if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) { + controllerListScreen.refreshControllers(); + } } private void onControllerDisconnect(int jid) { @@ -352,6 +355,10 @@ public class Controlify implements ControlifyApi { Component.translatable("controlify.toast.controller_disconnected.description", controller.name()), false ); + + if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) { + controllerListScreen.refreshControllers(); + } }); } diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java index 11ffd7a..911bf0a 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java @@ -16,6 +16,7 @@ import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.gui.DrawSize; import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionDescription; import net.minecraft.client.KeyMapping; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.locale.Language; @@ -171,12 +172,14 @@ public class ControllerBindingImpl implements Control Option.Builder> option = Option.createBuilder((Class>) (Class) IBind.class) .name(name()) .binding(defaultBind(), this::currentBind, this::setCurrentBind) - .tooltip(this.description()); + .description(OptionDescription.of(this.description())); if (controller instanceof GamepadController gamepad) { - ((Option.Builder>) (Object) option).controller(opt -> new GamepadBindController(opt, gamepad)); + ((Option.Builder>) (Object) option).customController(opt -> new GamepadBindController(opt, gamepad)); } else if (controller instanceof JoystickController joystick) { - ((Option.Builder>) (Object) option).controller(opt -> new JoystickBindController(opt, joystick)); + ((Option.Builder>) (Object) option).customController(opt -> new JoystickBindController(opt, joystick)); + } else { + throw new IllegalStateException("Unknown controller type: " + controller.getClass().getName()); } return option; diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/YACLScreenProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/YACLScreenProcessor.java index d26a68e..e25c93c 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/yacl/YACLScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/YACLScreenProcessor.java @@ -15,19 +15,9 @@ public class YACLScreenProcessor extends ScreenProcessor { @Override protected void handleButtons(Controller controller) { if (controller.bindings().GUI_ABSTRACT_ACTION_1.justPressed()) { - this.playClackSound(); - 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); + playClackSound(); +// if (screen.tabManager.getCurrentTab() instanceof ) +// screen.finishedSaveButton.onPress(); } super.handleButtons(controller); @@ -35,11 +25,11 @@ public class YACLScreenProcessor extends ScreenProcessor { @Override 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 protected void setInitialFocus() { - screen.setFocused(screen.optionList); +// screen.setFocused(screen.optionList); } } diff --git a/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java b/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java new file mode 100644 index 0000000..83686a9 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java @@ -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 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 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) { + + } + } +} diff --git a/src/main/java/dev/isxander/controlify/config/gui/ControllerConfigGui.java b/src/main/java/dev/isxander/controlify/config/gui/ControllerConfigGui.java new file mode 100644 index 0000000..bfe5b50 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/gui/ControllerConfigGui.java @@ -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 percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); + private static final Function 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.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.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.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.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.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 holdToggleFormatter = v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold")); + + return OptionGroup.createBuilder() + .name(Component.literal("Controls")) + .option(Option.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.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.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.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.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.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.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.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 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() + .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.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 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 all) { + all.forEach(pair -> ((AbstractBindController) pair.option().controller()).setConflicting(false)); + + for (OptionBindPair opt : all) { + var ctxs = BindContext.flatten(opt.binding().contexts()); + + List 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> strengthOptions = new ArrayList<>(); + Option allowVibrationOption; + vibrationGroup.option(allowVibrationOption = Option.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.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 gyroSensitivity; + List> 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.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.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.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> groupBindings(Collection 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) { + } +} diff --git a/src/main/java/dev/isxander/controlify/config/gui/GlobalSettingsGui.java b/src/main/java/dev/isxander/controlify/config/gui/GlobalSettingsGui.java new file mode 100644 index 0000000..d7a21ef --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/gui/GlobalSettingsGui.java @@ -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.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.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.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.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.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.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); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java b/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java index 262be5c..61038fa 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java +++ b/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java @@ -6,6 +6,6 @@ import com.terraformersmc.modmenu.api.ModMenuApi; public class ModMenuIntegration implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return YACLHelper::openConfigScreen; + return ControllerCarouselScreen::createConfigScreen; } } diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java deleted file mode 100644 index c24f751..0000000 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ /dev/null @@ -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 percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); - private static final Function 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 globalVibrationOption; - - var globalSettings = Controlify.instance().config().globalSettings(); - var globalCategory = ConfigCategory.createBuilder() - .name(Component.translatable("controlify.gui.category.global")) - .option(Option.createBuilder((Class>) (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 ConfigCategory createControllerCategory(Controller controller, Option 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 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 all) { - all.forEach(pair -> ((AbstractBindController) pair.option().controller()).setConflicting(false)); - - for (OptionBindPair opt : all) { - var ctxs = BindContext.flatten(opt.binding().contexts()); - - List 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 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> strengthOptions = new ArrayList<>(); - Option 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 gyroSensitivity; - List> 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 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> groupBindings(Collection bindings) { - return bindings.stream() - .collect(Collectors.groupingBy(ControllerBinding::category, LinkedHashMap::new, Collectors.toList())); - } - - private record OptionBindPair(Option option, ControllerBinding binding) { - } -} diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index 39773f2..a0b9fa3 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -7,6 +7,8 @@ import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.rumble.RumbleCapable; import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleSource; +import net.minecraft.resources.ResourceLocation; + import java.util.Optional; public interface Controller { @@ -41,6 +43,8 @@ public interface Controller hidInfo(); + ResourceLocation icon(); + default boolean canBeUsed() { return true; } @@ -116,6 +120,11 @@ public interface Controller extends Controller { JoystickMapping mapping(); + @Override + default ResourceLocation icon() { + return Controlify.id("textures/gui/joystick/icon.png"); + } + @Deprecated int axisCount(); @Deprecated diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java index 11f5af0..752b4b9 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java @@ -1,7 +1,7 @@ package dev.isxander.controlify.mixins.feature.settingsbutton; 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.gui.components.Button; import net.minecraft.client.gui.screens.OptionsSubScreen; @@ -22,7 +22,7 @@ public class ControlsScreenMixin extends OptionsSubScreen { @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) { - 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) .width(150) .build()); diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java index 6da3dfb..5b8b5fa 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java @@ -195,6 +195,8 @@ public class AbstractContainerScreenProcessor c.config().showScreenGuide).orElse(false); + List renderables = ((ScreenAccessor) screen).getRenderables(); if (leftLayout == null || rightLayout == null) diff --git a/src/main/java/dev/isxander/controlify/utils/Animator.java b/src/main/java/dev/isxander/controlify/utils/Animator.java new file mode 100644 index 0000000..cdafad5 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/Animator.java @@ -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 animations; + private final UnaryOperator easingFunction; + private final int durationTicks; + private float time; + + public Animator(int durationTicks, UnaryOperator easingFunction) { + this.animations = new ArrayList<>(); + this.easingFunction = easingFunction; + this.durationTicks = durationTicks; + } + + public void addConsumer(Consumer consumer, float start, float end) { + animations.add(new AnimationConsumer(consumer, start, end)); + } + + public void addConsumer(Consumer 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 consumer, float start, float end) { + } +} diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/dualsense/icon.png b/src/main/resources/assets/controlify/textures/gui/gamepad/dualsense/icon.png new file mode 100644 index 0000000..4185625 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/gamepad/dualsense/icon.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/steam_deck/icon.png b/src/main/resources/assets/controlify/textures/gui/gamepad/steam_deck/icon.png new file mode 100644 index 0000000..435cfe2 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/gamepad/steam_deck/icon.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/switch/icon.png b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/icon.png new file mode 100644 index 0000000..6f968f6 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/gamepad/switch/icon.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/gamepad/xbox_one/icon.png b/src/main/resources/assets/controlify/textures/gui/gamepad/xbox_one/icon.png new file mode 100644 index 0000000..82342ed Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/gamepad/xbox_one/icon.png differ diff --git a/src/main/resources/assets/controlify/textures/gui/joystick/.gitkeep b/src/main/resources/assets/controlify/textures/gui/joystick/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/assets/controlify/textures/screenshots/ingame-button-guide.png b/src/main/resources/assets/controlify/textures/screenshots/ingame-button-guide.png new file mode 100644 index 0000000..5566c5e Binary files /dev/null and b/src/main/resources/assets/controlify/textures/screenshots/ingame-button-guide.png differ diff --git a/src/main/resources/assets/controlify/textures/screenshots/reduce-aim-sensitivity.webp b/src/main/resources/assets/controlify/textures/screenshots/reduce-aim-sensitivity.webp new file mode 100644 index 0000000..0415843 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/screenshots/reduce-aim-sensitivity.webp differ diff --git a/src/main/resources/assets/controlify/textures/screenshots/screen-button-guide.webp b/src/main/resources/assets/controlify/textures/screenshots/screen-button-guide.webp new file mode 100644 index 0000000..8108b12 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/screenshots/screen-button-guide.webp differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3d81d5d..b4e47dd 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -34,7 +34,7 @@ "fabricloader": ">=0.14.0", "minecraft": ">1.20-", "java": ">=17", - "yet_another_config_lib": "^2.4.0" + "yet_another_config_lib": ">=3.0.0-" }, "breaks": { "midnightcontrols": "*"