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..ec165a3 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java @@ -3,12 +3,14 @@ package dev.isxander.controlify.api.bind; import com.google.gson.JsonObject; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.IBind; +import dev.isxander.controlify.bindings.RadialIcons; import dev.isxander.yacl3.api.Option; import net.minecraft.client.KeyMapping; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; +import java.util.Optional; import java.util.Set; import java.util.function.BooleanSupplier; @@ -43,6 +45,8 @@ public interface ControllerBinding { */ boolean justReleased(); + void fakePress(); + Component name(); Component description(); Component category(); @@ -66,6 +70,10 @@ public interface ControllerBinding { JsonObject toJson(); + void tick(); + + Optional radialIcon(); + record KeyMappingOverride(KeyMapping keyMapping, BooleanSupplier toggleable) { } } diff --git a/src/main/java/dev/isxander/controlify/api/bind/ControllerBindingBuilder.java b/src/main/java/dev/isxander/controlify/api/bind/ControllerBindingBuilder.java index a1f24c0..91e9152 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControllerBindingBuilder.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControllerBindingBuilder.java @@ -1,9 +1,6 @@ package dev.isxander.controlify.api.bind; -import dev.isxander.controlify.bindings.BindContext; -import dev.isxander.controlify.bindings.ControllerBindingImpl; -import dev.isxander.controlify.bindings.GamepadBinds; -import dev.isxander.controlify.bindings.IBind; +import dev.isxander.controlify.bindings.*; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.ControllerState; import net.minecraft.client.KeyMapping; @@ -76,6 +73,8 @@ public interface ControllerBindingBuilder { ControllerBindingBuilder context(BindContext... contexts); + ControllerBindingBuilder radialCandidate(ResourceLocation icon); + /** * Specifies are vanilla override for the binding. * Will emulate presses of the vanilla keybind when the controller binding is pressed. diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java index ea39de1..06c2d03 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java @@ -25,10 +25,7 @@ import net.minecraft.resources.ResourceLocation; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.ApiStatus; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.BooleanSupplier; public class ControllerBindingImpl implements ControllerBinding { @@ -39,11 +36,14 @@ public class ControllerBindingImpl implements Control private final ResourceLocation id; private final Component name, description, category; private final Set contexts; + private final ResourceLocation radialIcon; private final KeyMappingOverride override; private static final Map, Set>> pressedBinds = new HashMap<>(); - private ControllerBindingImpl(Controller controller, IBind defaultBind, ResourceLocation id, KeyMappingOverride vanillaOverride, Component name, Component description, Component category, Set contexts) { + private int fakePressState = 0; + + private ControllerBindingImpl(Controller controller, IBind defaultBind, ResourceLocation id, KeyMappingOverride vanillaOverride, Component name, Component description, Component category, Set contexts, ResourceLocation icon) { this.controller = controller; this.bind = this.defaultBind = defaultBind; this.renderer = new BindRendererImpl(bind); @@ -53,33 +53,38 @@ public class ControllerBindingImpl implements Control this.description = description; this.category = category; this.contexts = ImmutableSet.copyOf(contexts); + this.radialIcon = icon; } @Override public float state() { + if (fakePressState == 1) + return 1f; return bind.state(controller.state()); } @Override public float prevState() { + if (fakePressState == 2) + return 1f; return bind.state(controller.prevState()); } @Override public boolean held() { - return bind.held(controller.state()); + return fakePressState == 2 || bind.held(controller.state()); } @Override public boolean prevHeld() { - return bind.held(controller.prevState()); + return fakePressState == 3 || bind.held(controller.prevState()); } @Override public boolean justPressed() { if (hasBindPressed(this)) return false; - if (held() && !prevHeld()) { + if ((held() && !prevHeld()) || fakePressState == 2) { addPressedBind(this); return true; } else { @@ -91,7 +96,7 @@ public class ControllerBindingImpl implements Control public boolean justReleased() { if (hasBindPressed(this)) return false; - if (!held() && prevHeld()) { + if ((!held() && prevHeld()) || fakePressState == 3) { addPressedBind(this); return true; } else { @@ -99,6 +104,24 @@ public class ControllerBindingImpl implements Control } } + @Override + public void fakePress() { + this.fakePressState = 1; + } + + @Override + public void tick() { + if (fakePressState > 0) + fakePressState++; + if (fakePressState >= 4) + fakePressState = 0; + } + + @Override + public Optional radialIcon() { + return Optional.ofNullable(this.radialIcon); + } + public IBind currentBind() { return bind; } @@ -214,6 +237,7 @@ public class ControllerBindingImpl implements Control private Component name = null, description = null, category = null; private KeyMappingOverride override = null; private final Set contexts = new HashSet<>(); + private ResourceLocation radialIcon = null; public ControllerBindingBuilderImpl(Controller controller) { this.controller = controller; @@ -271,6 +295,12 @@ public class ControllerBindingImpl implements Control return this; } + @Override + public ControllerBindingBuilder radialCandidate(ResourceLocation icon) { + this.radialIcon = icon; + return this; + } + @Override public ControllerBindingBuilder vanillaOverride(KeyMapping keyMapping, BooleanSupplier toggleable) { this.override = new KeyMappingOverride(keyMapping, toggleable); @@ -299,7 +329,7 @@ public class ControllerBindingImpl implements Control } } - return new ControllerBindingImpl<>(controller, bind, id, override, name, description, category, contexts); + return new ControllerBindingImpl<>(controller, bind, id, override, name, description, category, contexts, radialIcon); } } diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index 3fede0d..a4abdba 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -20,6 +20,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.ToggleKeyMapping; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.item.Items; import java.util.*; import java.util.function.BooleanSupplier; @@ -37,6 +39,7 @@ public class ControllerBindings { public static final Component VMOUSE_CATEGORY = Component.translatable("controlify.binding_category.vmouse"); public static final Component GUI_CATEGORY = Component.translatable("controlify.binding_category.gui"); public static final Component MISC_CATEGORY = Component.translatable("key.categories.misc"); + public static final Component RADIAL_CATEGORY = Component.translatable("controlify.gui.radial_menu"); public final ControllerBinding WALK_FORWARD, WALK_BACKWARD, WALK_LEFT, WALK_RIGHT, @@ -59,6 +62,7 @@ public class ControllerBindings { PICK_BLOCK, TOGGLE_HUD_VISIBILITY, SHOW_PLAYER_LIST, + RADIAL_MENU, RADIAL_AXIS_UP, RADIAL_AXIS_DOWN, RADIAL_AXIS_LEFT, RADIAL_AXIS_RIGHT, 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, @@ -138,6 +142,7 @@ public class ControllerBindings { .defaultBind(GamepadBinds.A_BUTTON) .category(MOVEMENT_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getEffect(MobEffects.JUMP)) .build()); register(SPRINT = ControllerBindingBuilder.create(controller) .identifier("controlify", "sprint") @@ -171,12 +176,14 @@ public class ControllerBindings { .defaultBind(GamepadBinds.DPAD_DOWN) .category(GAMEPLAY_CATEGORY) .context(BindContexts.INGAME, BindContexts.INVENTORY) + .radialCandidate(RadialIcons.getItem(Items.BARRIER)) .build()); register(DROP_STACK = ControllerBindingBuilder.create(controller) .identifier("controlify", "drop_stack") .defaultBind(new EmptyBind<>()) .category(GAMEPLAY_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.TNT)) .build()); register(NEXT_SLOT = ControllerBindingBuilder.create(controller) .identifier("controlify", "next_slot") @@ -195,30 +202,35 @@ public class ControllerBindings { .defaultBind(GamepadBinds.START) .category(GAMEPLAY_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.STRUCTURE_VOID)) .build()); register(INVENTORY = ControllerBindingBuilder.create(controller) .identifier("controlify", "inventory") .defaultBind(GamepadBinds.Y_BUTTON) .category(INVENTORY_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.CHEST)) .build()); register(CHANGE_PERSPECTIVE = ControllerBindingBuilder.create(controller) .identifier("controlify", "change_perspective") .defaultBind(GamepadBinds.BACK) .category(GAMEPLAY_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.PAINTING)) .build()); register(SWAP_HANDS = ControllerBindingBuilder.create(controller) .identifier("controlify", "swap_hands") .defaultBind(GamepadBinds.X_BUTTON) .category(INVENTORY_CATEGORY) - .context(BindContexts.INGAME, BindContexts.INVENTORY) + .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.BONE)) .build()); register(OPEN_CHAT = ControllerBindingBuilder.create(controller) .identifier("controlify", "open_chat") .defaultBind(GamepadBinds.DPAD_UP) .category(MISC_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.WRITABLE_BOOK)) .vanillaOverride(options.keyChat, () -> false) .build()); register(GUI_PRESS = ControllerBindingBuilder.create(controller) @@ -280,6 +292,7 @@ public class ControllerBindings { .defaultBind(GamepadBinds.DPAD_LEFT) .category(GAMEPLAY_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.STICK)) .vanillaOverride(options.keyPickItem, () -> false) .build()); register(TOGGLE_HUD_VISIBILITY = ControllerBindingBuilder.create(controller) @@ -287,12 +300,44 @@ public class ControllerBindings { .defaultBind(new EmptyBind<>()) .category(MISC_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getEffect(MobEffects.INVISIBILITY)) .build()); register(SHOW_PLAYER_LIST = ControllerBindingBuilder.create(controller) .identifier("controlify", "show_player_list") - .defaultBind(GamepadBinds.DPAD_RIGHT) + .defaultBind(new EmptyBind<>()) .category(MISC_CATEGORY) .context(BindContexts.INGAME) + .radialCandidate(RadialIcons.getItem(Items.PLAYER_HEAD)) + .build()); + register(RADIAL_MENU = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_menu") + .defaultBind(GamepadBinds.DPAD_RIGHT) + .category(RADIAL_CATEGORY) + .context(BindContexts.INGAME) + .build()); + register(RADIAL_AXIS_UP = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_axis_up") + .defaultBind(GamepadBinds.RIGHT_STICK_FORWARD) + .category(RADIAL_CATEGORY) + .context(BindContexts.GUI) + .build()); + register(RADIAL_AXIS_DOWN = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_axis_down") + .defaultBind(GamepadBinds.RIGHT_STICK_BACKWARD) + .category(RADIAL_CATEGORY) + .context(BindContexts.GUI) + .build()); + register(RADIAL_AXIS_LEFT = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_axis_left") + .defaultBind(GamepadBinds.RIGHT_STICK_LEFT) + .category(RADIAL_CATEGORY) + .context(BindContexts.GUI) + .build()); + register(RADIAL_AXIS_RIGHT = ControllerBindingBuilder.create(controller) + .identifier("controlify", "radial_axis_right") + .defaultBind(GamepadBinds.RIGHT_STICK_RIGHT) + .category(RADIAL_CATEGORY) + .context(BindContexts.GUI) .build()); register(VMOUSE_MOVE_UP = ControllerBindingBuilder.create(controller) .identifier("controlify", "vmouse_move_up") @@ -408,6 +453,9 @@ public class ControllerBindings { this.imitateVanillaClick(); } }); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + registry().values().forEach(ControllerBinding::tick); + }); ControlifyEvents.INPUT_MODE_CHANGED.register(mode -> KeyMapping.releaseAll()); } @@ -503,6 +551,7 @@ public class ControllerBindings { .name(Component.translatable(keyMapping.getName())) .description(Component.translatable("controlify.custom_binding.vanilla_description").withStyle(ChatFormatting.GRAY)) .category(Component.translatable(keyMapping.getCategory())) + .radialCandidate(RadialIcons.FABRIC_ICON) .vanillaOverride(keyMapping, toggleOverride) .build(); 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..da57ff4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/bindings/RadialIcons.java @@ -0,0 +1,92 @@ +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.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import java.util.HashMap; +import java.util.Map; + +public final class RadialIcons { + private static final Minecraft minecraft = Minecraft.getInstance(); + + public static final ResourceLocation EMPTY = new ResourceLocation("controlify", "empty"); + public static final ResourceLocation FABRIC_ICON = new ResourceLocation("fabric-resource-loader-v0", "icon.png"); + + private static final Map icons = Util.make(() -> { + Map map = new HashMap<>(); + + map.put(EMPTY, (graphics, x, y) -> {}); + map.put(FABRIC_ICON, (graphics, x, y) -> { + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(0.5f, 0.5f, 1f); + graphics.blit(FABRIC_ICON, 0, 0, 0, 0, 32, 32, 32, 32); + graphics.pose().popPose(); + }); + addItems(map); + addPotionEffects(map); + + return map; + }); + + public static Map getIcons() { + return icons; + } + + public static ResourceLocation getItem(Item item) { + return prefixLocation("item", BuiltInRegistries.ITEM.getKey(item)); + } + + public static ResourceLocation getEffect(MobEffect effect) { + return prefixLocation("effect", BuiltInRegistries.MOB_EFFECT.getKey(effect)); + } + + 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 cb1c877..b91eb74 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -10,6 +10,7 @@ import dev.isxander.controlify.utils.Log; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.VersionParsingException; +import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -29,6 +30,7 @@ public class ControlifyConfig { .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeHierarchyAdapter(Class.class, new TypeAdapters.ClassTypeAdapter()) .registerTypeHierarchyAdapter(Version.class, new TypeAdapters.VersionTypeAdapter()) + .registerTypeHierarchyAdapter(ResourceLocation.class, new ResourceLocation.Serializer()) .create(); private final Controlify controlify; @@ -175,8 +177,8 @@ public class ControlifyConfig { private void applyControllerConfig(Controller controller, JsonObject object) { try { - controller.setConfig(GSON, object.getAsJsonObject("config")); dirty |= !controller.bindings().fromJson(object.getAsJsonObject("bindings")); + controller.setConfig(GSON, object.getAsJsonObject("config")); } catch (Exception e) { Log.LOGGER.error("Failed to load controller data for " + controller.uid() + ". Resetting to default!", e); controller.resetConfig(); diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index c254108..b910582 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -120,6 +120,7 @@ public abstract class AbstractController bindings) { + boolean changed = false; + for (int i = 0; i < radialActions.length; i++) { + ResourceLocation action = radialActions[i]; + if (!RadialMenuScreen.EMPTY_ACTION.equals(action) && (action == null || !bindings.registry().containsKey(action) || bindings.registry().get(action).radialIcon().isEmpty())) { + setDefaultRadialAction(bindings, i); + changed = true; + } + } + if (changed) + Controlify.instance().config().setDirty(); + + return !changed; + } + + private void setDefaultRadialAction(ControllerBindings bindings, int index) { + radialActions[index] = switch (index) { + case 0 -> bindings.TOGGLE_HUD_VISIBILITY.id(); + case 1 -> bindings.CHANGE_PERSPECTIVE.id(); + case 2 -> bindings.DROP_STACK.id(); + case 3 -> bindings.OPEN_CHAT.id(); + case 4 -> bindings.SWAP_HANDS.id(); + case 5 -> bindings.PICK_BLOCK.id(); + case 6 -> bindings.PAUSE.id(); + case 7 -> bindings.SHOW_PLAYER_LIST.id(); + default -> RadialMenuScreen.EMPTY_ACTION; + }; + } } diff --git a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java index 6baafe0..f4b3f3b 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java @@ -193,6 +193,11 @@ public class InGameButtonGuide implements IngameGuideRegistry { return Optional.of(Component.translatable("controlify.guide.ingame.inventory")); return Optional.empty(); }); + registerGuideAction(controller.bindings().RADIAL_MENU, ActionLocation.RIGHT, ctx -> { + if (ctx.client().screen == null) + return Optional.of(Component.translatable("controlify.gui.radial_menu")); + return Optional.empty(); + }); registerGuideAction(controller.bindings().ATTACK, ActionLocation.RIGHT, (ctx) -> { var hitResult = ctx.hitResult(); if (hitResult.getType() == HitResult.Type.ENTITY) 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 f8269c2..e7c3a56 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -1,5 +1,6 @@ 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; @@ -16,10 +17,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; @@ -334,6 +332,20 @@ public class ControllerConfigScreenFactory { .name(Component.translatable("controlify.gui.group.controls")); List optionBinds = new ArrayList<>(); + + ButtonOption editRadialButton = ButtonOption.createBuilder() + .name(Component.translatable("controlify.gui.radial_menu")) + .description(OptionDescription.of(Component.translatable("controlify.gui.radial_menu.tooltip"))) + .action((screen, opt) -> Minecraft.getInstance().setScreen(new RadialMenuScreen(controller, true, screen))) + .text(Component.translatable("controlify.gui.radial_menu.btn_text")) + .build(); + Option radialBind = controller.bindings().RADIAL_MENU.startYACLOption() + .listener((opt, val) -> updateConflictingBinds(optionBinds)) + .build(); + optionBinds.add(new OptionBindPair(radialBind, controller.bindings().RADIAL_MENU)); + category.option(editRadialButton); + category.option(radialBind); + groupBindings(controller.bindings().registry().values()).forEach((categoryName, bindGroup) -> { var controlsGroup = OptionGroup.createBuilder() .name(categoryName); 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..1698935 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/RadialMenuScreen.java @@ -0,0 +1,522 @@ +package dev.isxander.controlify.gui.screen; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.api.bind.BindRenderer; +import dev.isxander.controlify.api.bind.ControllerBinding; +import dev.isxander.controlify.bindings.RadialIcons; +import dev.isxander.controlify.controller.Controller; +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.ComponentProcessor; +import dev.isxander.controlify.screenop.ScreenControllerEventListener; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.sound.ControlifySounds; +import dev.isxander.controlify.utils.Animator; +import dev.isxander.controlify.utils.Easings; +import dev.isxander.controlify.virtualmouse.VirtualMouseBehaviour; +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.NarratedElementType; +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.CommonComponents; +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.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class RadialMenuScreen extends Screen implements ScreenControllerEventListener, ScreenProcessorProvider { + public static final ResourceLocation EMPTY_ACTION = new ResourceLocation("controlify", "empty_action"); + + private final Controller controller; + private final boolean editMode; + private final Screen parent; + + private final RadialButton[] buttons = new RadialButton[8]; + private int selectedButton = -1; + private int idleTicks; + private boolean isEditing; + + private ActionSelectList actionSelectList; + + private final Processor processor = new Processor(this); + + public RadialMenuScreen(Controller controller, boolean editMode, Screen parent) { + super(Component.empty()); + this.controller = controller; + this.editMode = editMode; + this.parent = parent; + } + + @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, (float) radialButton.getX()); + animation.addConsumer(radialButton::setY, centerY - 16, (float) radialButton.getY()); + } + Animator.INSTANCE.play(animation); + + if (editMode) { + var exitGuide = addRenderableWidget(new PositionedComponent<>( + new GuideActionRenderer<>( + new GuideAction<>( + controller.bindings().GUI_BACK, + obj -> Optional.of(CommonComponents.GUI_DONE) + ), + false, + true + ), + AnchorPoint.BOTTOM_CENTER, + 0, -10, + AnchorPoint.BOTTOM_CENTER + )); + + exitGuide.getComponent().updateName(null); + exitGuide.updatePosition(width, height); + } + } + + @Override + public void onControllerInput(Controller controller) { + if (this.controller != controller) return; + + if (!editMode && !controller.bindings().RADIAL_MENU.held()) { + if (selectedButton != -1 && buttons[selectedButton].invoke()) { + playClickSound(); + } + + onClose(); + } + + if (editMode && controller.bindings().GUI_BACK.justPressed()) { + playClickSound(); + onClose(); + } + + if (!isEditing) { + float x = controller.bindings().RADIAL_AXIS_RIGHT.state() - controller.bindings().RADIAL_AXIS_LEFT.state(); + float y = controller.bindings().RADIAL_AXIS_DOWN.state() - controller.bindings().RADIAL_AXIS_UP.state(); + 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++) { + boolean selected = i == selectedButton; + buttons[i].setFocused(selected); + if (selected) { + this.setFocused(buttons[i]); + } + } + + idleTicks = 0; + } else if (!editMode) { + idleTicks++; + if (idleTicks >= 20) { + selectedButton = -1; + for (RadialButton button : buttons) { + button.setFocused(false); + } + } + } + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (editMode) + renderDirtBackground(graphics); + + super.render(graphics, mouseX, mouseY, delta); + + if (!editMode) { + graphics.drawCenteredString( + font, + Component.translatable("controlify.radial_menu.configure_hint"), + width / 2, + height - 39, + -1 + ); + } + } + + private void playClickSound() { + minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1f)); + } + + private void finishEditing() { + isEditing = false; + removeWidget(actionSelectList); + this.setFocused(null); + actionSelectList = null; + } + + @Override + public void onClose() { + Controlify.instance().config().saveIfDirty(); + minecraft.setScreen(parent); + } + + @Override + public boolean isPauseScreen() { + return editMode; + } + + @Override + public ScreenProcessor screenProcessor() { + return this.processor; + } + + public class RadialButton implements Renderable, GuiEventListener, NarratableEntry, ComponentProcessor { + public static final ResourceLocation TEXTURE = Controlify.id("textures/gui/radial-buttons.png"); + + private int x, y; + private float translateX, translateY; + private boolean focused; + private ControllerBinding binding; + private MultiLineLabel name; + private RadialIcons.Icon icon; + + private RadialButton(int index, float x, float y) { + this.setX(x); + this.setY(y); + + this.setAction(controller.config().radialActions[index]); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + graphics.pose().pushPose(); + graphics.pose().translate(x + translateX, y + translateY, 0); + + graphics.pose().pushPose(); + graphics.pose().scale(2, 2, 1); + graphics.blit(TEXTURE, 0, 0, focused ? 16 : 0, 0, 16, 16, 32, 16); + graphics.pose().popPose(); + + if (!editMode || !focused) { + graphics.pose().pushPose(); + graphics.pose().translate(4, 4, 0); + graphics.pose().scale(1.5f, 1.5f, 1); + this.icon.draw(graphics, 0, 0); + graphics.pose().popPose(); + } else { + BindRenderer renderer = controller.bindings().GUI_PRESS.renderer(); + renderer.render(graphics, 16 - renderer.size().width() / 2, 16); + } + + 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 void setAction(ResourceLocation binding) { + if (!EMPTY_ACTION.equals(binding)) { + this.binding = controller.bindings().get(binding); + this.icon = RadialIcons.getIcons().get(this.binding.radialIcon().orElseThrow()); + this.name = MultiLineLabel.create(font, this.binding.name(), 76); + } else { + this.binding = null; + this.name = MultiLineLabel.EMPTY; + this.icon = RadialIcons.getIcons().get(RadialIcons.EMPTY); + } + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public void setX(float x) { + this.x = (int) x; + this.translateX = x - this.x; + } + + public void setY(float y) { + this.y = (int) y; + this.translateY = y - this.y; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (editMode && controller == RadialMenuScreen.this.controller && controller.bindings().GUI_PRESS.justPressed()) { + 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); + RadialMenuScreen.this.setFocused(actionSelectList); + isEditing = true; + return true; + } + return false; + } + + @Override + public NarrationPriority narrationPriority() { + return isFocused() ? NarrationPriority.FOCUSED : NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + if (binding != null) + builder.add(NarratedElementType.TITLE, binding.name()); + } + + @Override + public ScreenRectangle getRectangle() { + return new ScreenRectangle(x, y, 32, 32); + } + } + + public class ActionSelectList implements Renderable, ContainerEventHandler, NarratableEntry, ComponentProcessor { + 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().entrySet().stream() + .filter(entry -> entry.getValue().radialIcon().isPresent()) + .map(Map.Entry::getKey) + .forEach(id -> children.add(new ActionEntry(id))); + + var selectedBind = controller.config().radialActions[radialIndex]; + children.stream() + .filter(action -> action.binding.equals(selectedBind)) + .findAny() + .ifPresent(this::setFocused); + + } + + @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 boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (controller == RadialMenuScreen.this.controller) { + if (controller.bindings().GUI_BACK.justPressed()) { + finishEditing(); + return true; + } + } + + return false; + } + + @Override + public List children() { + return children; + } + + @Override + public boolean isDragging() { + return false; + } + + @Override + public void setDragging(boolean dragging) { + + } + + @Nullable + @Override + public ActionEntry 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); + } + } + } + + @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) { + if (getFocused() != null) { + builder.add(NarratedElementType.TITLE, getFocused().name); + } + } + + public class ActionEntry implements GuiEventListener, ComponentProcessor { + 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, focused ? -1 : 0xffa6a6a6); + } + + @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); + } + + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) { + if (controller == RadialMenuScreen.this.controller) { + if (controller.bindings().GUI_PRESS.justPressed()) { + controller.config().radialActions[radialIndex] = binding; + Controlify.instance().config().setDirty(); + + buttons[radialIndex].setAction(binding); + + playClickSound(); + finishEditing(); + return true; + } + } + + return false; + } + } + } + + public static class Processor extends ScreenProcessor { + public Processor(RadialMenuScreen screen) { + super(screen); + } + + @Override + public VirtualMouseBehaviour virtualMouseBehaviour() { + return VirtualMouseBehaviour.DISABLED; + } + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java index f8f7b0c..fe49484 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; @@ -105,7 +106,13 @@ public class InGameInputHandler { minecraft.options.hideGui = !minecraft.options.hideGui; } - shouldShowPlayerList = controller.bindings().SHOW_PLAYER_LIST.held(); + if (controller.bindings().SHOW_PLAYER_LIST.justPressed()) { + shouldShowPlayerList = !shouldShowPlayerList; + } + + if (controller.bindings().RADIAL_MENU.justPressed()) { + minecraft.setScreen(new RadialMenuScreen(controller, false, null)); + } } 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/java/dev/isxander/controlify/mixins/feature/screenop/ContainerEventHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/ContainerEventHandlerMixin.java new file mode 100644 index 0000000..29d89b9 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/ContainerEventHandlerMixin.java @@ -0,0 +1,19 @@ +package dev.isxander.controlify.mixins.feature.screenop; + +import dev.isxander.controlify.screenop.CustomFocus; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ContainerEventHandler.class) +public interface ContainerEventHandlerMixin extends CustomFocus { + @Shadow + @Nullable GuiEventListener getFocused(); + + @Override + default GuiEventListener getCustomFocus() { + return this.getFocused(); + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 740662b..f9d78e7 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -113,6 +113,9 @@ "controlify.gui.test_vibration": "Test Vibration", "controlify.gui.test_vibration.tooltip": "Test the vibration of your controller.", + "controlify.gui.radial_menu": "Radial Menu", + "controlify.gui.radial_menu.tooltip": "Open up the radial menu to configure what each button does.", + "controlify.gui.radial_menu.btn_text": "CONFIGURE", "controlify.gui.group.controls": "Controls", "controlify.gui.group.controls.tooltip": "Adjust the controller controls.", "controlify.gui.bind_input_awaiting": "Press any button", @@ -218,6 +221,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", @@ -286,6 +290,8 @@ "controlify.calibration.later": "Maybe Later", "controlify.calibration.later.tooltip": "You must calibrate to use the controller. Pressing this will deactivate the controller and you will have to use it again to calibrate.", + "controlify.radial_menu.configure_hint": "Configure actions in the controller settings.", + "controlify.controller_submission.title": "Unknown Controller Submission", "controlify.controller_submission.message": "Please submit some of your controller info to Controlify's database to get it added in a future update.\n\nControlify sends the following information:\n- Your controller's vendor & product IDs\n- Your controller's GUID\n- The name of your controller (in the box below)\n- The version of Controlify you are currently on\n\nThis is completely anonymous and doesn't store any of your personal or account information.", "controlify.controller_submission.operational_checkbox": "Does your controller work with Controlify?", 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 0000000..798cdc8 Binary files /dev/null and b/src/main/resources/assets/controlify/textures/gui/radial-buttons.png differ diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 9701dff..d8871fb 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -27,6 +27,7 @@ "core.MouseHandlerMixin", "feature.accessibility.LocalPlayerMixin", "feature.autoswitch.ToastComponentAccessor", + "feature.bind.GuiMixin", "feature.bind.KeyMappingAccessor", "feature.bind.KeyMappingMixin", "feature.bind.ToggleKeyMappingAccessor", @@ -52,6 +53,7 @@ "feature.rumble.levelevents.LevelRendererMixin", "feature.rumble.useitem.LivingEntityMixin", "feature.rumble.useitem.LocalPlayerMixin", + "feature.screenop.ContainerEventHandlerMixin", "feature.screenop.GameRendererMixin", "feature.screenop.MinecraftMixin", "feature.screenop.ScreenAccessor",