From 3800bf0f71c97a97c29eeda9e91862d09740d544 Mon Sep 17 00:00:00 2001 From: Ocelot Date: Tue, 19 May 2026 14:56:39 -0600 Subject: [PATCH] Add partial shoulder surfing compat * Implements compat for shoulder surfing * The shoulder surfing camera angle is incorrect when riding on a sub-level --- .../src/main/groovy/multiloader-common.gradle | 1 + .../new_camera_types/CameraTypeMixin.java | 2 +- .../shouldersurfing/EntityHelperMixin.java | 19 +++ .../shouldersurfing/ObjectPickerMixin.java | 26 +++++ .../shouldersurfing/PerspectiveMixin.java | 110 ++++++++++++++++++ .../ShoulderSurfingCameraMixin.java | 20 ++++ .../ShoulderSurfingImplMixin.java | 31 +++++ .../new_camera_types/SableCameraTypes.java | 6 +- .../shouldersurfing/SablePerspectives.java | 12 ++ common/src/main/resources/sable.mixins.json | 5 + fabric/build.gradle | 1 + gradle.properties | 1 + neoforge/build.gradle | 3 +- 13 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/EntityHelperMixin.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ObjectPickerMixin.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/PerspectiveMixin.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingCameraMixin.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingImplMixin.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixinhelpers/compatibility/shouldersurfing/SablePerspectives.java diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle index ccabc7ec..8a85f5c1 100644 --- a/buildSrc/src/main/groovy/multiloader-common.gradle +++ b/buildSrc/src/main/groovy/multiloader-common.gradle @@ -112,6 +112,7 @@ dependencies { compileOnly("maven.modrinth:jade-addons-forge:${project.jade_addons_version}+neoforge") { transitive = false } compileOnly("maven.modrinth:moonlight:${project.moonlight_version}-neoforge") compileOnly("curse.maven:vista-1368607:$vista_version") + compileOnly("maven.modrinth:shoulder-surfing-reloaded:${minecraft_version}-${project.shouldersurfing_version}+neoforge") { transitive = false } } // Declare capabilities on the outgoing configurations. diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/camera/new_camera_types/CameraTypeMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/camera/new_camera_types/CameraTypeMixin.java index da7fb9e1..b41d69e8 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/camera/new_camera_types/CameraTypeMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/camera/new_camera_types/CameraTypeMixin.java @@ -41,7 +41,7 @@ public class CameraTypeMixin { @Invoker(value = "") private static CameraType create(final String name, final int ordinal, final boolean firstPerson, final boolean mirrored) { - throw new IllegalStateException("Unreachable"); + throw new AssertionError("Unreachable"); } /** diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/EntityHelperMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/EntityHelperMixin.java new file mode 100644 index 00000000..57f06e14 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/EntityHelperMixin.java @@ -0,0 +1,19 @@ +package dev.ryanhcode.sable.mixin.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.api.util.EntityHelper; +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.Sable; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(EntityHelper.class) +public class EntityHelperMixin { + + @ModifyVariable(method = "lookAtTarget", at = @At("HEAD"), index = 1, argsOnly = true) + private static Vec3 modifyTarget(final Vec3 original, @Local(argsOnly = true) final LocalPlayer player) { + return Sable.HELPER.projectOutOfSubLevel(player.level(), original); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ObjectPickerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ObjectPickerMixin.java new file mode 100644 index 00000000..ee752b2e --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ObjectPickerMixin.java @@ -0,0 +1,26 @@ +package dev.ryanhcode.sable.mixin.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.api.model.PickContext; +import com.github.exopandora.shouldersurfing.client.ObjectPicker; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.Sable; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ObjectPicker.class) +public class ObjectPickerMixin { + + @Redirect(method = "pick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D")) + public double distanceTo(final Vec3 instance, final Vec3 vec, @Local(argsOnly = true) final Player player) { + return Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(player.level(), instance, vec)); + } + + @Redirect(method = "pickEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D")) + public double distanceToSq(final Vec3 instance, final Vec3 vec, @Local(argsOnly = true) final PickContext context) { + return Sable.HELPER.distanceSquaredWithSubLevels(context.entity().level(), instance, vec); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/PerspectiveMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/PerspectiveMixin.java new file mode 100644 index 00000000..89e36d9c --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/PerspectiveMixin.java @@ -0,0 +1,110 @@ +package dev.ryanhcode.sable.mixin.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.api.client.IClientConfig; +import com.github.exopandora.shouldersurfing.api.model.CrosshairVisibility; +import com.github.exopandora.shouldersurfing.api.model.Perspective; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.lib.apache.commons.ArrayUtils; +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.mixinhelpers.camera.new_camera_types.SableCameraTypes; +import dev.ryanhcode.sable.mixinhelpers.compatibility.shouldersurfing.SablePerspectives; +import net.minecraft.client.CameraType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Perspective.class) +public class PerspectiveMixin { + + @Shadow + @Final + @Mutable + private static Perspective[] $VALUES; + + static { + final var subLevelView = create("SUB_LEVEL_VIEW", $VALUES.length, SableCameraTypes.SUB_LEVEL_VIEW, CrosshairVisibility.NEVER); + + $VALUES = ArrayUtils.add($VALUES, subLevelView); + + final var subLevelViewUnlocked = create("SUB_LEVEL_VIEW_UNLOCKED", $VALUES.length, SableCameraTypes.SUB_LEVEL_VIEW_UNLOCKED, CrosshairVisibility.NEVER); + + $VALUES = ArrayUtils.add($VALUES, subLevelViewUnlocked); + } + + @SuppressWarnings("SameParameterValue") + @Invoker(value = "") + private static Perspective create(final String name, final int ordinal, final CameraType cameraType, final CrosshairVisibility defaultCrosshairVisibility) { + throw new AssertionError("Unreachable"); + } + + @SuppressWarnings("ConstantValue") + @WrapOperation(method = "next", at = @At(value = "INVOKE", target = "Lcom/github/exopandora/shouldersurfing/api/client/IClientConfig;replaceDefaultPerspective()Z")) + public boolean nextPerspective(final IClientConfig instance, final Operation original) { + if ((Object) this == SablePerspectives.SUB_LEVEL_VIEW || (Object) this == SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED) { + return false; + } + return original.call(instance); + } + + @ModifyVariable(method = "next", at = @At(value = "STORE"), name = "next") + public Perspective next(final Perspective next, @Local(argsOnly = true) final IClientConfig config) { + if (config.replaceDefaultPerspective()) { + if ((Object) this == Perspective.SHOULDER_SURFING) { + return SablePerspectives.SUB_LEVEL_VIEW; + } + } else { + // The normal logic will try to wrap around to our new values, but the next one should be first person + if ((Object) this == Perspective.SHOULDER_SURFING) { + return Perspective.FIRST_PERSON; + } + + if ((Object) this == Perspective.THIRD_PERSON_BACK) { + return SablePerspectives.SUB_LEVEL_VIEW; + } + } + + if ((Object) this == SablePerspectives.SUB_LEVEL_VIEW) { + return SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED; + } + if ((Object) this == SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED) { + return Perspective.THIRD_PERSON_FRONT; + } + + return next; + } + + @Inject(method = "next", at = @At("TAIL"), cancellable = true) + public void getNext(final CallbackInfoReturnable cir, @Local(name = "next") final Perspective next) { + if (next == SablePerspectives.SUB_LEVEL_VIEW) { + cir.setReturnValue(SablePerspectives.SUB_LEVEL_VIEW); + } + if (next == SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED) { + cir.setReturnValue(SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED); + } + } + + @SuppressWarnings("ConstantValue") + @Inject(method = "isEnabled", at = @At("HEAD"), cancellable = true) + public void isEnabled(final CallbackInfoReturnable cir) { + if ((Object) this == SablePerspectives.SUB_LEVEL_VIEW || (Object) this == SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED) { + cir.setReturnValue(true); + } + } + + @Inject(method = "of", at = @At("HEAD"), cancellable = true) + private static void of(final CameraType cameraType, final boolean shoulderSurfing, final CallbackInfoReturnable cir) { + if (cameraType == SableCameraTypes.SUB_LEVEL_VIEW) { + cir.setReturnValue(SablePerspectives.SUB_LEVEL_VIEW); + } + if (cameraType == SableCameraTypes.SUB_LEVEL_VIEW_UNLOCKED) { + cir.setReturnValue(SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED); + } + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingCameraMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingCameraMixin.java new file mode 100644 index 00000000..35f820fd --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingCameraMixin.java @@ -0,0 +1,20 @@ +package dev.ryanhcode.sable.mixin.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.client.ShoulderSurfingCamera; +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.Sable; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ShoulderSurfingCamera.class) +public class ShoulderSurfingCameraMixin { + + @Redirect(method = "maxZoom", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D")) + private static double distanceTo(final Vec3 instance, final Vec3 vec, @Local(argsOnly = true) final BlockGetter level) { + return level instanceof Level ? Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels((Level) level, instance, vec)) : instance.distanceTo(vec); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingImplMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingImplMixin.java new file mode 100644 index 00000000..9383d519 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/shouldersurfing/ShoulderSurfingImplMixin.java @@ -0,0 +1,31 @@ +package dev.ryanhcode.sable.mixin.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.api.client.IClientConfig; +import com.github.exopandora.shouldersurfing.api.model.Perspective; +import com.github.exopandora.shouldersurfing.client.ShoulderSurfingImpl; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.mixinhelpers.compatibility.shouldersurfing.SablePerspectives; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ShoulderSurfingImpl.class) +public class ShoulderSurfingImplMixin { + + @WrapOperation(method = "togglePerspective", at = @At(value = "INVOKE", target = "Lcom/github/exopandora/shouldersurfing/api/model/Perspective;next(Lcom/github/exopandora/shouldersurfing/api/client/IClientConfig;)Lcom/github/exopandora/shouldersurfing/api/model/Perspective;")) + public Perspective next(final Perspective instance, final IClientConfig config, final Operation original) { + final Entity cameraEntity = Minecraft.getInstance().cameraEntity; + + Perspective next = original.call(instance, config); + while (next == SablePerspectives.SUB_LEVEL_VIEW || next == SablePerspectives.SUB_LEVEL_VIEW_UNLOCKED) { + if (cameraEntity != null && Sable.HELPER.getVehicleSubLevel(cameraEntity) != null) { + break; + } + next = original.call(next, config); + } + return next; + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/camera/new_camera_types/SableCameraTypes.java b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/camera/new_camera_types/SableCameraTypes.java index 7c7ae3e7..e98da5cd 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/camera/new_camera_types/SableCameraTypes.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/camera/new_camera_types/SableCameraTypes.java @@ -2,7 +2,11 @@ import net.minecraft.client.CameraType; -public class SableCameraTypes { +public final class SableCameraTypes { + public static final CameraType SUB_LEVEL_VIEW = Enum.valueOf(CameraType.class, "SUB_LEVEL_VIEW"); public static final CameraType SUB_LEVEL_VIEW_UNLOCKED = Enum.valueOf(CameraType.class, "SUB_LEVEL_VIEW_UNLOCKED"); + + private SableCameraTypes() { + } } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/compatibility/shouldersurfing/SablePerspectives.java b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/compatibility/shouldersurfing/SablePerspectives.java new file mode 100644 index 00000000..54f4a539 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/compatibility/shouldersurfing/SablePerspectives.java @@ -0,0 +1,12 @@ +package dev.ryanhcode.sable.mixinhelpers.compatibility.shouldersurfing; + +import com.github.exopandora.shouldersurfing.api.model.Perspective; + +public class SablePerspectives { + + public static final Perspective SUB_LEVEL_VIEW = Enum.valueOf(Perspective.class, "SUB_LEVEL_VIEW"); + public static final Perspective SUB_LEVEL_VIEW_UNLOCKED = Enum.valueOf(Perspective.class, "SUB_LEVEL_VIEW_UNLOCKED"); + + private SablePerspectives() { + } +} diff --git a/common/src/main/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index 45d68a7f..056a61ef 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -17,6 +17,7 @@ "clip_overwrite.ClientLevelMixin", "clip_overwrite.GameRendererMixin", "compatibility.iris.ExtendedShaderMixin", + "compatibility.shouldersurfing.ObjectPickerMixin", "config.GameRendererAccessor", "debug_render.DebugRendererMixin", "debug_render.DebugScreenOverlayMixin", @@ -102,6 +103,10 @@ "compatibility.jade.BlockAccessorImplMixin", "compatibility.jade.RayTracingMixin", "compatibility.jadeaddons.CreatePluginMixin", + "compatibility.shouldersurfing.EntityHelperMixin", + "compatibility.shouldersurfing.PerspectiveMixin", + "compatibility.shouldersurfing.ShoulderSurfingCameraMixin", + "compatibility.shouldersurfing.ShoulderSurfingImplMixin", "compatibility.vista.LODMixin", "compatibility.vista.ViewFinderAccessMixin", "compatibility.vista.ViewFinderControllerMixin", diff --git a/fabric/build.gradle b/fabric/build.gradle index 9a743453..3a5175c8 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -24,6 +24,7 @@ dependencies { modCompileOnly "net.caffeinemc:sodium-fabric:${rootProject.sodium_version}" modCompileOnly "maven.modrinth:distanthorizons:${rootProject.distant_horizons_version}" modCompileOnly "maven.modrinth:sodium-extras:fabric-${minecraft_version}-$sodiumextras_version" + modCompileOnly "maven.modrinth:shoulder-surfing-reloaded:${minecraft_version}-${project.shouldersurfing_version}+fabric" include(modApi("foundry.veil:veil-fabric-${project.minecraft_version}:${project.veil_version}")) modCompileOnly("foundry.imguimc:imguimc-fabric-${project.minecraft_version}:${project.imguimc_version}") diff --git a/gradle.properties b/gradle.properties index 3a735280..c1ae5c5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -56,6 +56,7 @@ iris_version=1.8.12+1.21.1-neoforge distant_horizons_version=2.2.1-a-1.21.1 nml_version=1.4.3 sodiumextras_version=1.0.8 +shouldersurfing_version=4.22.10 loom.ignoreDependencyLoomVersionValidation=true diff --git a/neoforge/build.gradle b/neoforge/build.gradle index dedc829a..697a374d 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -82,9 +82,10 @@ dependencies { implementation("com.tterrag.registrate:Registrate:${registrate_version}") compileOnly("maven.modrinth:protomanlys-weather:${pmweather_version}") - compileOnly("io.github.mortuusars.exposure:exposure-${minecraft_version}-neoforge:${exposure_version}") { transitive = false } + compileOnly("io.github.mortuusars.exposure:exposure-${minecraft_version}-neoforge:${project.exposure_version}") { transitive = false } compileOnly("maven.modrinth:jade:${project.jade_version}+neoforge") { transitive = false } compileOnly("maven.modrinth:jade-addons-forge:${project.jade_addons_version}+neoforge") { transitive = false } + compileOnly("maven.modrinth:shoulder-surfing-reloaded:${minecraft_version}-${project.shouldersurfing_version}+neoforge") { transitive = false } compileOnly "net.caffeinemc:sodium-neoforge-mod:${sodium_version}" compileOnly("maven.modrinth:iris:$iris_version") { transitive = false }