diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index ca9bc6e..38849c5 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.config; import com.google.common.collect.Lists; +import dev.isxander.controlify.reacharound.ReachAroundMode; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import java.util.List; @@ -14,4 +15,5 @@ public class GlobalSettings { public boolean keyboardMovement = false; public boolean outOfFocusInput = false; + public ReachAroundMode reachAround = ReachAroundMode.OFF; } diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index 9885f37..3d5201e 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -14,6 +14,7 @@ import dev.isxander.controlify.controller.joystick.JoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; +import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.yacl.api.*; import dev.isxander.yacl.gui.controllers.ActionController; import dev.isxander.yacl.gui.controllers.BooleanController; @@ -53,7 +54,14 @@ public class YACLHelper { .tooltip(Component.translatable("controlify.gui.current_controller.tooltip")) .binding(Controlify.instance().currentController(), () -> Controlify.instance().currentController(), v -> Controlify.instance().setCurrentController(v)) .controller(opt -> new CyclingListController<>(opt, Iterables.concat(List.of(Controller.DUMMY), Controller.CONTROLLERS.values().stream().filter(Controller::canBeUsed).toList()), c -> Component.literal(c == Controller.DUMMY ? "Disabled" : c.name()))) - .instant(true) + .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.out_of_focus_input")) diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/reacharound/GameRendererMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/reacharound/GameRendererMixin.java new file mode 100644 index 0000000..0e64931 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/reacharound/GameRendererMixin.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.mixins.feature.reacharound; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import dev.isxander.controlify.reacharound.ReachAroundHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.world.phys.HitResult; +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(GameRenderer.class) +public class GameRendererMixin { + @Shadow @Final Minecraft minecraft; + + @ModifyExpressionValue(method = "pick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;pick(DFZ)Lnet/minecraft/world/phys/HitResult;")) + private HitResult modifyPick(HitResult hitResult) { + return ReachAroundHandler.getReachAroundHitResult(minecraft.getCameraEntity(), hitResult); + } +} diff --git a/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java new file mode 100644 index 0000000..ec594a8 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java @@ -0,0 +1,43 @@ +package dev.isxander.controlify.reacharound; + +import dev.isxander.controlify.Controlify; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; + +public class ReachAroundHandler { + public static HitResult getReachAroundHitResult(Entity entity, HitResult hitResult) { + // if there is already a valid hit, we don't want to override it + if (hitResult.getType() != HitResult.Type.MISS) + return hitResult; + + if (!canReachAround(entity)) + return hitResult; + + // LivingEntity#playBlockFallSound - this is the location where the game determines the footstep noise + // maybe experiment on different values rather than 0.2f from other areas in the game? + int x = Mth.floor(entity.getX()); + int y = Mth.floor(entity.getY() - 0.2F); + int z = Mth.floor(entity.getZ()); + var floorPos = new BlockPos(x, y, z); + + // this allows all interaction with blocks, such as opening containers, ringing bells, etc. + // this is consistent with bedrock edition behaviour, tested + return new BlockHitResult(floorPos.getCenter(), entity.getDirection(), floorPos, false); + } + + private static boolean canReachAround(Entity cameraEntity) { + return // don't want to place blocks while riding an entity + cameraEntity.getVehicle() == null + // straight ahead = 0deg, up = -90deg, down = 90deg + // 45deg and above is half between straight ahead and down, must be lower or equal to this threshold + && cameraEntity.getXRot() >= 45 + // if the player is not standing on a block, this is inappropriate + // this also prevents selecting fluids as a valid position + && cameraEntity.isOnGround() + // must respect config option + && Controlify.instance().config().globalSettings().reachAround.canReachAround(); + } +} diff --git a/src/main/java/dev/isxander/controlify/reacharound/ReachAroundMode.java b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundMode.java new file mode 100644 index 0000000..156ac74 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundMode.java @@ -0,0 +1,32 @@ +package dev.isxander.controlify.reacharound; + +import dev.isxander.controlify.Controlify; +import dev.isxander.yacl.api.NameableEnum; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +import java.util.function.BiFunction; + +public enum ReachAroundMode implements NameableEnum { + OFF((minecraft, controlify) -> false), + SINGLEPLAYER_ONLY((minecraft, controlify) -> minecraft.isSingleplayer()), + SINGLEPLAYER_AND_LAN((minecraft, controlify) -> minecraft.isLocalServer()), + EVERYWHERE((minecraft, controlify) -> true); + + private final BiFunction canReachAround; + private final Component displayName; + + ReachAroundMode(BiFunction canReachAround) { + this.canReachAround = canReachAround; + this.displayName = Component.translatable("controlify.reach_around." + this.name().toLowerCase()); + } + + public boolean canReachAround() { + return canReachAround.apply(Minecraft.getInstance(), Controlify.instance()); + } + + @Override + public Component getDisplayName() { + return displayName; + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index fc959d5..ec4e833 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -2,6 +2,14 @@ "controlify.gui.category.global": "Global", "controlify.gui.current_controller": "Current Controller", "controlify.gui.current_controller.tooltip": "In Controlify's infancy, only one controller can be used at a time, this selects which one you want to use.", + "controlify.gui.reach_around": "Block Reach Around", + "controlify.gui.reach_around.tooltip": "If enabled, you can interact with the block you are standing on in the direction you are looking.", + "controlify.gui.reach_around.tooltip.parity": "This is parity with bedrock edition where you can also do this.", + "controlify.gui.reach_around.tooltip.warning": "WARNING: This is an unfair advantage over other players without Controlify, and you will likely be flagged by many anti-cheats. This should only be used in situations where everyone playing recognises that you have this ability and are okay with it.", + "controlify.reach_around.off": "Off", + "controlify.reach_around.singleplayer_only": "Singleplayer Only", + "controlify.reach_around.singleplayer_and_lan": "Singleplayer and LAN", + "controlify.reach_around.everywhere": "Everywhere", "controlify.gui.out_of_focus_input": "Out of Focus Input", "controlify.gui.out_of_focus_input.tooltip": "If enabled, Controlify will still receive input even if the game window is not focused.", "controlify.gui.keyboard_movement": "Keyboard-like Movement", diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 0e7c2f9..b984295 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -32,6 +32,7 @@ "feature.guide.screen.AbstractButtonMixin", "feature.guide.screen.AbstractWidgetMixin", "feature.guide.screen.TabNavigationBarMixin", + "feature.reacharound.GameRendererMixin", "feature.screenop.MinecraftMixin", "feature.screenop.ScreenMixin", "feature.screenop.vanilla.AbstractButtonMixin",