From 8efac5bdef5d4500cdda9a794726011794dd033f Mon Sep 17 00:00:00 2001 From: isXander Date: Mon, 24 Jul 2023 22:19:53 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9E=95=20Start=20work=20on=20radial=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/bind/ControllerBinding.java | 2 + .../bindings/ControllerBindingImpl.java | 15 +- .../bindings/ControllerBindings.java | 7 + .../controlify/bindings/RadialAction.java | 11 + .../controlify/bindings/RadialIcons.java | 80 ++++ .../controlify/config/ControlifyConfig.java | 2 + .../controller/ControllerConfig.java | 14 + .../gui/layout/PositionedComponent.java | 26 +- .../screen/ControllerConfigScreenFactory.java | 23 +- .../gui/screen/RadialMenuScreen.java | 424 ++++++++++++++++++ .../controlify/ingame/InGameInputHandler.java | 5 + .../mixins/feature/bind/GuiMixin.java | 21 + .../assets/controlify/lang/en_us.json | 3 + .../textures/gui/radial-buttons.png | Bin 0 -> 261 bytes src/main/resources/controlify.mixins.json | 19 +- 15 files changed, 636 insertions(+), 16 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/bindings/RadialAction.java create mode 100644 src/main/java/dev/isxander/controlify/bindings/RadialIcons.java create mode 100644 src/main/java/dev/isxander/controlify/gui/screen/RadialMenuScreen.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/bind/GuiMixin.java create mode 100644 src/main/resources/assets/controlify/textures/gui/radial-buttons.png diff --git a/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java b/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java index 752d84c..6392a77 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java @@ -43,6 +43,8 @@ public interface ControllerBinding { */ boolean justReleased(); + void fakePress(); + Component name(); Component description(); Component category(); diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java index 8450f23..e23769d 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java @@ -43,6 +43,8 @@ public class ControllerBindingImpl implements Control private static final Map, Set>> pressedBinds = new HashMap<>(); + private boolean fakePress, fakeRelease; + private ControllerBindingImpl(Controller controller, IBind defaultBind, ResourceLocation id, KeyMappingOverride vanillaOverride, Component name, Component description, Component category, Set contexts) { this.controller = controller; this.bind = this.defaultBind = defaultBind; @@ -67,6 +69,11 @@ public class ControllerBindingImpl implements Control @Override public boolean held() { + if (fakePress) { + fakePress = false; + return true; + } + return bind.held(controller.state()); } @@ -79,7 +86,8 @@ public class ControllerBindingImpl implements Control public boolean justPressed() { if (hasBindPressed(this)) return false; - if (held() && !prevHeld()) { + if ((held() && !prevHeld()) || fakePress) { + fakePress = false; addPressedBind(this); return true; } else { @@ -99,6 +107,11 @@ public class ControllerBindingImpl implements Control } } + @Override + public void fakePress() { + this.fakePress = true; + } + public IBind currentBind() { return bind; } diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index 339c917..37dfb3c 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -59,6 +59,7 @@ public class ControllerBindings { PICK_BLOCK, TOGGLE_HUD_VISIBILITY, SHOW_PLAYER_LIST, + RADIAL_MENU, VMOUSE_MOVE_UP, VMOUSE_MOVE_DOWN, VMOUSE_MOVE_LEFT, VMOUSE_MOVE_RIGHT, VMOUSE_LCLICK, VMOUSE_RCLICK, VMOUSE_SHIFT_CLICK, VMOUSE_SCROLL_UP, VMOUSE_SCROLL_DOWN, @@ -295,6 +296,12 @@ public class ControllerBindings { .category(MISC_CATEGORY) .context(BindContexts.INGAME) .build()); + register(RADIAL_MENU = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_menu") + .defaultBind(GamepadBinds.DPAD_DOWN) + .category(MISC_CATEGORY) + .context(BindContexts.INGAME) + .build()); register(VMOUSE_MOVE_UP = ControllerBindingBuilder.create(controller) .identifier("controlify", "vmouse_move_up") .defaultBind(GamepadBinds.LEFT_STICK_FORWARD) diff --git a/src/main/java/dev/isxander/controlify/bindings/RadialAction.java b/src/main/java/dev/isxander/controlify/bindings/RadialAction.java new file mode 100644 index 0000000..09dc21b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/RadialAction.java @@ -0,0 +1,11 @@ +package dev.isxander.controlify.bindings; + +import dev.isxander.controlify.gui.screen.RadialMenuScreen; +import net.minecraft.resources.ResourceLocation; + +public record RadialAction(ResourceLocation binding, ResourceLocation icon) { + public static final RadialAction EMPTY = new RadialAction( + RadialMenuScreen.EMPTY, + RadialIcons.EMPTY + ); +} diff --git a/src/main/java/dev/isxander/controlify/bindings/RadialIcons.java b/src/main/java/dev/isxander/controlify/bindings/RadialIcons.java new file mode 100644 index 0000000..b5898ec --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/RadialIcons.java @@ -0,0 +1,80 @@ +package dev.isxander.controlify.bindings; + +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.MobEffectTextureManager; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; + +import java.util.HashMap; +import java.util.Map; + +public final class RadialIcons { + private static final Minecraft minecraft = Minecraft.getInstance(); + + public static final ResourceLocation EMPTY = new ResourceLocation("controlify", "empty"); + + private static final Map icons = Util.make(() -> { + Map map = new HashMap<>(); + + map.put(EMPTY, (graphics, x, y) -> {}); + addItems(map); + addPotionEffects(map); + + return map; + }); + + public static Map getIcons() { + return icons; + } + + private static void addItems(Map map) { + BuiltInRegistries.ITEM.entrySet().forEach(entry -> { + ResourceKey key = entry.getKey(); + ItemStack stack = entry.getValue().getDefaultInstance(); + + map.put(prefixLocation("item", key.location()), (graphics, x, y) -> { + graphics.renderItem(stack, x, y); + }); + }); + } + + private static void addPotionEffects(Map map) { + MobEffectTextureManager mobEffectTextureManager = minecraft.getMobEffectTextures(); + + BuiltInRegistries.MOB_EFFECT.entrySet().forEach(entry -> { + ResourceKey key = entry.getKey(); + MobEffect effect = entry.getValue(); + + TextureAtlasSprite sprite = mobEffectTextureManager.get(effect); + map.put(prefixLocation("effect", key.location()), (graphics, x, y) -> { + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(0.88f, 0.88f, 1f); + + graphics.blit(0, 0, 0, 18, 18, sprite); + + graphics.pose().popPose(); + }); + }); + } + + private static ResourceLocation prefixLocation(String prefix, ResourceLocation location) { + return new ResourceLocation(location.getNamespace(), prefix + "/" + location.getPath()); + } + + @FunctionalInterface + public interface Icon { + void draw(GuiGraphics graphics, int x, int y); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index b4e1e3e..47fda1b 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -8,6 +8,7 @@ import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo; import dev.isxander.controlify.utils.DebugLog; import dev.isxander.controlify.utils.Log; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -26,6 +27,7 @@ public class ControlifyConfig { .setPrettyPrinting() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeHierarchyAdapter(Class.class, new ClassTypeAdapter()) + .registerTypeHierarchyAdapter(ResourceLocation.class, new ResourceLocation.Serializer()) .create(); private final Controlify controlify; diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java index 5220650..914b0f3 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java @@ -2,7 +2,10 @@ package dev.isxander.controlify.controller; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import dev.isxander.controlify.bindings.RadialAction; +import dev.isxander.controlify.gui.screen.RadialMenuScreen; import dev.isxander.controlify.rumble.RumbleSource; +import net.minecraft.resources.ResourceLocation; import java.io.Serializable; @@ -36,6 +39,17 @@ public abstract class ControllerConfig implements Serializable { public boolean mixedInput = false; + public RadialAction[] radialActions = new RadialAction[]{ + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + RadialAction.EMPTY, + }; + public abstract void setDeadzone(int axis, float deadzone); public abstract float getDeadzone(int axis); diff --git a/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java index b612e34..7caf0bb 100644 --- a/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java +++ b/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java @@ -1,11 +1,13 @@ package dev.isxander.controlify.gui.layout; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; import org.joml.Vector2ic; -public class PositionedComponent implements Renderable { +public class PositionedComponent implements Renderable, GuiEventListener, NarratableEntry { private final T component; private int x, y; @@ -52,4 +54,24 @@ public class PositionedComponent implements Renderabl public T getComponent() { return component; } + + @Override + public void setFocused(boolean focused) { + + } + + @Override + public boolean isFocused() { + return false; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index bb4bf7f..52390bd 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -1,9 +1,11 @@ package dev.isxander.controlify.gui.screen; +import com.google.common.collect.Iterables; 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.bindings.RadialAction; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerConfig; import dev.isxander.controlify.controller.gamepad.GamepadController; @@ -16,10 +18,7 @@ import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.RumbleSource; import dev.isxander.controlify.rumble.RumbleState; import dev.isxander.yacl3.api.*; -import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; -import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder; -import dev.isxander.yacl3.api.controller.StringControllerBuilder; -import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; +import dev.isxander.yacl3.api.controller.*; import net.minecraft.ChatFormatting; import net.minecraft.Util; import net.minecraft.client.Minecraft; @@ -309,6 +308,22 @@ public class ControllerConfigScreenFactory { var category = ConfigCategory.createBuilder() .name(Component.translatable("controlify.gui.group.controls")); + var radialMenuGroup = OptionGroup.createBuilder() + .name(Component.translatable("controlify.gui.group.radial_menu")) + .collapsed(true); + radialMenuGroup.option(controller.bindings().RADIAL_MENU.startYACLOption().build()); + for (int i = 0; i < controller.config().radialActions.length; i++) { + int action = i; + radialMenuGroup.option(Option.createBuilder() + .name(Component.translatable("controlify.gui.radial_menu_action", i + 1)) + .binding(RadialMenuScreen.EMPTY, () -> controller.config().radialActions[action].binding(), v -> controller.config().radialActions[action] = new RadialAction(v, controller.config().radialActions[action].icon())) + .controller(opt -> CyclingListControllerBuilder.create(opt) + .values(Iterables.concat(Collections.singleton(RadialMenuScreen.EMPTY), controller.bindings().registry().keySet())) + .valueFormatter(id -> !RadialMenuScreen.EMPTY.equals(id) ? controller.bindings().get(id).name() : Component.literal("None"))) + .build()); + } + category.group(radialMenuGroup.build()); + List optionBinds = new ArrayList<>(); groupBindings(controller.bindings().registry().values()).forEach((categoryName, bindGroup) -> { var controlsGroup = OptionGroup.createBuilder() diff --git a/src/main/java/dev/isxander/controlify/gui/screen/RadialMenuScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/RadialMenuScreen.java new file mode 100644 index 0000000..1ea46c1 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/RadialMenuScreen.java @@ -0,0 +1,424 @@ +package dev.isxander.controlify.gui.screen; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.api.bind.ControllerBinding; +import dev.isxander.controlify.api.guide.GuideActionNameSupplier; +import dev.isxander.controlify.bindings.GamepadBinds; +import dev.isxander.controlify.bindings.RadialAction; +import dev.isxander.controlify.bindings.RadialIcons; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.gamepad.GamepadController; +import dev.isxander.controlify.controller.gamepad.GamepadState; +import dev.isxander.controlify.gui.guide.GuideAction; +import dev.isxander.controlify.gui.guide.GuideActionRenderer; +import dev.isxander.controlify.gui.layout.AnchorPoint; +import dev.isxander.controlify.gui.layout.PositionedComponent; +import dev.isxander.controlify.screenop.ScreenControllerEventListener; +import dev.isxander.controlify.sound.ControlifySounds; +import dev.isxander.controlify.utils.Animator; +import dev.isxander.controlify.utils.Easings; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +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.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class RadialMenuScreen extends Screen implements ScreenControllerEventListener { + public static final ResourceLocation EMPTY = new ResourceLocation("controlify", "empty_action"); + + private final Controller controller; + + public RadialMenuScreen(Controller controller) { + super(Component.empty()); + this.controller = controller; + } + + private final RadialButton[] buttons = new RadialButton[8]; + private int selectedButton = -1; + private int idleTicks; + private boolean editMode; + private boolean isEditing; + + private PositionedComponent> editModeGuide; + private ActionSelectList actionSelectList; + + @Override + protected void init() { + int centerX = this.width / 2; + int centerY = this.height / 2; + + RadialButton button; + addRenderableWidget(buttons[0] = button = new RadialButton(0, centerX - 16, centerY - 64 - 8)); + addRenderableWidget(buttons[1] = button = new RadialButton(1, button.x + 32 + 8, button.y + 16)); + addRenderableWidget(buttons[2] = button = new RadialButton(2, button.x + 16, button.y + 32 + 8)); + addRenderableWidget(buttons[3] = button = new RadialButton(3, button.x - 16, button.y + 32 + 8)); + addRenderableWidget(buttons[4] = button = new RadialButton(4, button.x - 32 - 8, button.y + 16)); + addRenderableWidget(buttons[5] = button = new RadialButton(5, button.x - 32 - 8, button.y - 16)); + addRenderableWidget(buttons[6] = button = new RadialButton(6, button.x - 16, button.y - 32 - 8)); + addRenderableWidget(buttons[7] = new RadialButton(7, button.x + 16, button.y - 32 - 8)); + + Animator.AnimationInstance animation = new Animator.AnimationInstance(5, Easings::easeOutQuad); + for (RadialButton radialButton : buttons) { + animation.addConsumer(radialButton::setX, centerX - 16, radialButton.getX()); + animation.addConsumer(radialButton::setY, centerY - 16, radialButton.getY()); + } + Animator.INSTANCE.play(animation); + + editModeGuide = addRenderableWidget(new PositionedComponent<>( + new GuideActionRenderer<>( + new GuideAction<>( + controller.bindings().GUI_ABSTRACT_ACTION_2, + obj -> Optional.of(Component.literal(!editMode ? "Edit Mode" : "Done Editing")) + ), + false, + true + ), + AnchorPoint.BOTTOM_CENTER, + 0, -10, + AnchorPoint.BOTTOM_CENTER + )); + + editModeGuide.getComponent().updateName(null); + editModeGuide.updatePosition(width, height); + } + + @Override + public void onControllerInput(Controller controller) { + if (this.controller != controller) return; + + if (!controller.bindings().RADIAL_MENU.held()) { + if (!isEditing) { + if (!editMode) { + if (selectedButton != -1 && buttons[selectedButton].invoke()) { + playClickSound(); + } + + onClose(); + } else { + RadialButton button = buttons[selectedButton]; + int x = button.x < width / 2 ? button.x - 110 : button.x + 42; + actionSelectList = new ActionSelectList(selectedButton, x, button.y, 100, 80); + addRenderableWidget(actionSelectList); + setFocused(actionSelectList); + isEditing = true; + } + } + } + + if (controller.bindings().GUI_ABSTRACT_ACTION_2.justPressed()) { + editMode = !editMode; + editModeGuide.getComponent().updateName(null); + editModeGuide.updatePosition(width, height); + playClickSound(); + + if (!editMode) { + finishEditing(); + } + } + + if (isEditing) { + if (controller.bindings().GUI_PRESS.justPressed() || controller.bindings().GUI_BACK.justPressed()) { + finishEditing(); + } + } + + if (!isEditing && controller.state() instanceof GamepadState state) { + float x = state.gamepadAxes().leftStickX(); + float y = state.gamepadAxes().leftStickY(); + float threshold = controller.config().buttonActivationThreshold; + + if (Math.abs(x) >= threshold || Math.abs(y) >= threshold) { + float angle = Mth.wrapDegrees(Mth.RAD_TO_DEG * (float) Mth.atan2(y, x) - 90f) + 180f; + float each = 360f / buttons.length; + + int newSelected = Mth.floor((angle + each / 2f) / each) % buttons.length; + if (newSelected != selectedButton) { + selectedButton = newSelected; + minecraft.getSoundManager().play(SimpleSoundInstance.forUI(ControlifySounds.SCREEN_FOCUS_CHANGE, 1f)); + } + + for (int i = 0; i < buttons.length; i++) { + buttons[i].setFocused(i == selectedButton); + } + + idleTicks = 0; + } else { + idleTicks++; + if (idleTicks >= 20) { + selectedButton = -1; + for (RadialButton button : buttons) { + button.setFocused(false); + } + } + } + } + } + + private void playClickSound() { + minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1f)); + } + + private void finishEditing() { + isEditing = false; + onClose(); + } + + @Override + public void onClose() { + Controlify.instance().config().saveIfDirty(); + super.onClose(); + } + + public class RadialButton implements Renderable, GuiEventListener, NarratableEntry { + public static final ResourceLocation TEXTURE = Controlify.id("textures/gui/radial-buttons.png"); + + private int x, y; + private boolean focused; + private final ControllerBinding binding; + private final MultiLineLabel name; + private final RadialIcons.Icon icon; + + private RadialButton(int index, int x, int y) { + this.x = x; + this.y = y; + + RadialAction action = controller.config().radialActions[index]; + if (!EMPTY.equals(action.binding())) { + this.binding = controller.bindings().get(action.binding()); + this.name = MultiLineLabel.create(font, this.binding.name(), 76); + } else { + this.binding = null; + this.name = MultiLineLabel.EMPTY; + } + this.icon = RadialIcons.getIcons().get(action.icon()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(2, 2, 1); + graphics.blit(TEXTURE, 0, 0, focused ? 16 : 0, 0, 16, 16, 32, 16); + graphics.pose().popPose(); + + graphics.pose().pushPose(); + graphics.pose().translate(x + 4, y + 4, 0); + graphics.pose().scale(1.5f, 1.5f, 1); + this.icon.draw(graphics, 0, 0); + graphics.pose().popPose(); + + if (focused) + name.renderCentered(graphics, width / 2, height / 2 - font.lineHeight / 2 - ((name.getLineCount() - 1) * font.lineHeight / 2)); + } + + public boolean invoke() { + if (binding != null) { + binding.fakePress(); + return true; + } + return false; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public void setX(int x) { + this.x = x; + } + + public void setY(int y) { + this.y = y; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public NarrationPriority narrationPriority() { + return isFocused() ? NarrationPriority.FOCUSED : NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } + + @Override + public ScreenRectangle getRectangle() { + return new ScreenRectangle(x, y, 32, 32); + } + } + + public class ActionSelectList implements Renderable, ContainerEventHandler, NarratableEntry { + private final int radialIndex; + + private int x, y; + private int width, height; + private final int itemHeight = 10; + private int scrollOffset; + + private boolean focused; + private ActionEntry focusedEntry; + + private final List children = new ArrayList<>(); + + public ActionSelectList(int index, int x, int y, int width, int height) { + this.radialIndex = index; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + controller.bindings().registry().forEach((id, binding) -> { + children.add(new ActionEntry(id)); + }); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + graphics.fill(x, y, x + width, y + height, 0x80000000); + + graphics.enableScissor(x, y, x + width, y + height); + int y = this.y - scrollOffset; + for (ActionEntry child : children) { + child.render(graphics, x, y, width, itemHeight, mouseX, mouseY, delta); + y += itemHeight; + } + graphics.disableScissor(); + + graphics.renderOutline(x - 1, this.y - 1, width + 2, height + 2, 0x80ffffff); + } + + @Override + public List children() { + return children; + } + + @Override + public boolean isDragging() { + return false; + } + + @Override + public void setDragging(boolean dragging) { + + } + + @Nullable + @Override + public GuiEventListener getFocused() { + return focusedEntry; + } + + @Override + public void setFocused(@Nullable GuiEventListener child) { + ActionEntry focus = (ActionEntry) child; + this.focusedEntry = focus; + + if (focus != null) { + int index = children().indexOf(child); + if (index != -1) { + int focusY = index * itemHeight - scrollOffset; + if (focusY < 0) + scrollOffset = Mth.clamp(index * itemHeight, 0, children().size() * itemHeight - height); + else if (focusY + itemHeight > height) + scrollOffset = Mth.clamp(index * itemHeight + itemHeight - height, 0, children().size() * itemHeight - height); + } + + controller.config().radialActions[radialIndex] = new RadialAction(focus.binding, controller.config().radialActions[radialIndex].icon()); + Controlify.instance().config().setDirty(); + } + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Override + public NarrationPriority narrationPriority() { + return focused ? NarrationPriority.FOCUSED : NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } + + public class ActionEntry implements GuiEventListener { + private int x, y; + private boolean focused; + private final ResourceLocation binding; + private final Component name; + + public ActionEntry(ResourceLocation binding) { + this.binding = binding; + this.name = controller.bindings().get(binding).name(); + } + + public void render(GuiGraphics graphics, int x, int y, int width, int itemHeight, int mouseX, int mouseY, float delta) { + this.x = x; + this.y = y; + + if (focused) + graphics.fill(x, y, x + width, y + itemHeight, 0xff000000); + graphics.drawString(RadialMenuScreen.this.font, name, x + 2, y + 1, -1); + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent event) { + return !focused ? ComponentPath.leaf(this) : null; + } + + @Override + public ScreenRectangle getRectangle() { + return new ScreenRectangle(x, y, width, itemHeight); + } + } + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java index f8f7b0c..7501472 100644 --- a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java +++ b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java @@ -7,6 +7,7 @@ import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.gamepad.GamepadState; +import dev.isxander.controlify.gui.screen.RadialMenuScreen; import dev.isxander.controlify.utils.Animator; import dev.isxander.controlify.utils.Easings; import dev.isxander.controlify.utils.NavigationHelper; @@ -106,6 +107,10 @@ public class InGameInputHandler { } shouldShowPlayerList = controller.bindings().SHOW_PLAYER_LIST.held(); + + if (controller.bindings().RADIAL_MENU.justPressed()) { + minecraft.setScreen(new RadialMenuScreen(controller)); + } } protected void handlePlayerLookInput() { diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/bind/GuiMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/bind/GuiMixin.java new file mode 100644 index 0000000..5c8423d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/bind/GuiMixin.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.mixins.feature.bind; + +import com.llamalad7.mixinextras.injector.WrapWithCondition; +import dev.isxander.controlify.gui.screen.RadialMenuScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiGraphics; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Gui.class) +public class GuiMixin { + @Shadow @Final private Minecraft minecraft; + + @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;renderCrosshair(Lnet/minecraft/client/gui/GuiGraphics;)V")) + private boolean shouldRenderCrosshair(Gui instance, GuiGraphics graphics) { + return !(minecraft.screen instanceof RadialMenuScreen); + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 6714c78..6a2c754 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -109,6 +109,8 @@ "controlify.gui.test_vibration": "Test Vibration", "controlify.gui.test_vibration.tooltip": "Test the vibration of your controller.", + "controlify.gui.group.radial_menu": "Radial Menu", + "controlify.gui.radial_menu_action": "Action #%s", "controlify.gui.group.controls": "Controls", "controlify.gui.group.controls.tooltip": "Adjust the controller controls.", "controlify.gui.bind_input_awaiting": "Press any button", @@ -211,6 +213,7 @@ "controlify.binding.controlify.pick_block": "Pick Block", "controlify.binding.controlify.toggle_hud_visibility": "Toggle HUD Visibility", "controlify.binding.controlify.show_player_list": "Show Player List", + "controlify.binding.controlify.radial_menu": "Radial Menu", "controlify.binding.controlify.vmouse_move_up": "VMouse Move Up", "controlify.binding.controlify.vmouse_move_down": "VMouse Move Down", "controlify.binding.controlify.vmouse_move_left": "VMouse Move Left", diff --git a/src/main/resources/assets/controlify/textures/gui/radial-buttons.png b/src/main/resources/assets/controlify/textures/gui/radial-buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..798cdc8aa822fd34e11658b718d223d5671e7e06 GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYe6k~CayA#8@b22Z1oTZ*Fjv*eM zZ>KI4Jfa}t5`W1fMn=p%bY=6ZM;k0;LLAsF4^B|-+;M)U#5KdizjtSqKU&>v=ezxT ze2s0v%G(@^7>)jmTk5R9UkMNYtWbWT@ z`;6h7<+M~r4aR#{I1HG27?cn2EWJH7C`sda%{7B5dDo-5e@+oHbbDZ?7G(NNeBZU# z`bQ=7lS96qlBs7fJL3JVdfkGrKnL@sMtG+A`Z8z%*&IL&0+)g(gD6i|KbLh*2~7Y% CK4Jy{ literal 0 HcmV?d00001 diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 4caac03..4c8eb2e 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -7,37 +7,33 @@ }, "compatibilityLevel": "JAVA_17", "client": [ + "compat.fapi.KeyBindingRegistryImplAccessor", "compat.iris.BaseOptionElementWidgetMixin", "compat.sodium.CycleControlElementMixin", "compat.sodium.SliderControlElementMixin", - "compat.sodium.TickBoxControlElementMixin", - "core.GLXMixin", - "feature.fixes.boatfix.BoatMixin", - "feature.rumble.explosion.LightningBoltMixin", - "feature.rumble.fishing.FishingHookMixin", - "feature.rumble.itembreak.LivingEntityMixin", - "feature.rumble.levelevents.LevelRendererMixin", - "feature.rumble.useitem.LivingEntityMixin", - "compat.fapi.KeyBindingRegistryImplAccessor", "compat.sodium.SodiumOptionsGUIAccessor", "compat.sodium.SodiumOptionsGUIMixin", + "compat.sodium.TickBoxControlElementMixin", "compat.yacl.CyclingControllerElementMixin", "compat.yacl.SliderControllerElementMixin", "compat.yacl.YACLScreenCategoryTabAccessor", "compat.yacl.YACLScreenCategoryTabMixin", "compat.yacl.YACLScreenMixin", "core.ClientPacketListenerMixin", + "core.GLXMixin", "core.GuiMixin", "core.KeyboardHandlerMixin", "core.MinecraftMixin", "core.MouseHandlerMixin", "feature.accessibility.LocalPlayerMixin", "feature.autoswitch.ToastComponentAccessor", + "feature.bind.GuiMixin", "feature.bind.KeyMappingAccessor", "feature.bind.KeyMappingMixin", "feature.bind.ToggleKeyMappingAccessor", "feature.chatkbheight.ChatComponentMixin", "feature.chatkbheight.ChatScreenMixin", + "feature.fixes.boatfix.BoatMixin", "feature.fixes.boatfix.LocalPlayerMixin", "feature.guide.ingame.ClientPacketListenerMixin", "feature.guide.ingame.GuiMixin", @@ -50,7 +46,12 @@ "feature.rumble.blockbreak.MultiPlayerGameModeMixin", "feature.rumble.damage.LocalPlayerMixin", "feature.rumble.explosion.ClientPacketListenerMixin", + "feature.rumble.explosion.LightningBoltMixin", + "feature.rumble.fishing.FishingHookMixin", + "feature.rumble.itembreak.LivingEntityMixin", "feature.rumble.itembreak.LocalPlayerMixin", + "feature.rumble.levelevents.LevelRendererMixin", + "feature.rumble.useitem.LivingEntityMixin", "feature.rumble.useitem.LocalPlayerMixin", "feature.screenop.GameRendererMixin", "feature.screenop.MinecraftMixin",