From fbc1d04835460efd24acfc4dd46903f22f7f1e0f Mon Sep 17 00:00:00 2001 From: timtaran Date: Thu, 10 Sep 2026 20:25:58 +0500 Subject: [PATCH 1/5] fix: wrong sable crosshair position --- .../sable/SableCompatHelper.java | 25 +++++ .../internal/SableCompatHelperInternal.java | 106 ++++++++++++++++++ .../core/client/tasks/types/TaskSwing.java | 5 +- .../client/renderer/GameRendererMixin.java | 15 ++- .../ryanhcode/sable/ActiveSableCompanion.java | 16 +++ .../main/java/dev/ryanhcode/sable/Sable.java | 10 ++ .../sable/companion/SableCompanion.java | 11 ++ .../sable/companion/math/Pose3dc.java | 5 + .../sable/sublevel/ClientSubLevel.java | 9 ++ .../ryanhcode/sable/sublevel/SubLevel.java | 9 ++ 10 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/SableCompatHelper.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/internal/SableCompatHelperInternal.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/Sable.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/SableCompanion.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/math/Pose3dc.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/ClientSubLevel.java create mode 100644 visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/SubLevel.java diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/SableCompatHelper.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/SableCompatHelper.java new file mode 100644 index 00000000..062292ea --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/SableCompatHelper.java @@ -0,0 +1,25 @@ +package org.vmstudio.visor.compatibility.sable; + +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniond; +import org.vmstudio.visor.api.ModLoader; +import org.vmstudio.visor.compatibility.sable.internal.SableCompatHelperInternal; + +public final class SableCompatHelper { + public static final String MOD_ID = "sable"; + + public static boolean isLoaded() { + return ModLoader.get().isModLoaded(MOD_ID); + } + + public static Vec3 toWorldPos(@Nullable Level level, @Nullable HitResult hitResult, Vec3 fallback) { + return SableCompatHelperInternal.toWorldPos(level, hitResult, fallback); + } + + public static @Nullable Quaterniond getSubLevelOrientation(@Nullable Level level, Vec3 pos) { + return SableCompatHelperInternal.getSubLevelOrientation(level, pos); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/internal/SableCompatHelperInternal.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/internal/SableCompatHelperInternal.java new file mode 100644 index 00000000..8dd1dd6e --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/sable/internal/SableCompatHelperInternal.java @@ -0,0 +1,106 @@ +package org.vmstudio.visor.compatibility.sable.internal; + +import dev.ryanhcode.sable.companion.math.Pose3dc; +import dev.ryanhcode.sable.sublevel.ClientSubLevel; +import dev.ryanhcode.sable.sublevel.SubLevel; +import net.minecraft.core.Position; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniond; +import org.vmstudio.visor.api.common.utils.LoggerUtils; +import org.vmstudio.visor.compatibility.OneShotSetup; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * This class should not be called anywhere outside SableCompatHelper to ensure JVM will not load any sable classes unless it's installed. + * @see org.vmstudio.visor.compatibility.sable.SableCompatHelper + */ +public class SableCompatHelperInternal { + private static final OneShotSetup SETUP = new OneShotSetup(SableCompatHelperInternal::resolve); + + private static Object helperInstance; + private static Method projectOutOfSubLevelMethod; + private static Method getContainingMethod; + + public static Vec3 toWorldPos(@Nullable Level level, @Nullable HitResult hitResult, Vec3 fallback) { + if (!SETUP.ok() || level == null || !(hitResult instanceof BlockHitResult)) { + return fallback; + } + + try { + Vec3 location = hitResult.getLocation(); + Object projected = projectOutOfSubLevelMethod.invoke(helperInstance, level, location); + if (projected instanceof Vec3 vec3) { + return vec3; + } + } catch (Throwable t) { + LoggerUtils.getLogger().warn("Visor: failed to project position out of Sable sub-level", t); + } + + return fallback; + } + + public static @Nullable Quaterniond getSubLevelOrientation(@Nullable Level level, Vec3 pos) { + if (!SETUP.ok()) { + return null; + } + + try { + Object subLevelObject = getContainingMethod.invoke(helperInstance, level, pos); + if (subLevelObject == null) { + return null; + } + SubLevel subLevel = (SubLevel) subLevelObject; + + Pose3dc pose; + if (subLevel instanceof ClientSubLevel clientSubLevel) { + pose = clientSubLevel.renderPose(); + } else { + pose = subLevel.logicalPose(); + } + + return (Quaterniond) pose.getClass().getMethod("orientation").invoke(pose); + } catch (Throwable t) { + LoggerUtils.getLogger().warn("Visor: failed to get rotation of Sable sub-level", t); + } + + return null; + } + + private static boolean resolve() throws ReflectiveOperationException { + Class sableClass = Class.forName("dev.ryanhcode.sable.Sable"); + Field helperField = sableClass.getField("HELPER"); + helperInstance = helperField.get(null); + if (helperInstance == null) { + return false; + } + + Class companionClass = helperInstance.getClass(); + + for (Method m : companionClass.getMethods()) { + if ("projectOutOfSubLevel".equals(m.getName()) && m.getParameterCount() == 2) { + Class[] params = m.getParameterTypes(); + if (Level.class.isAssignableFrom(params[0]) && Position.class.isAssignableFrom(params[1])) { + projectOutOfSubLevelMethod = m; + } + } + } + + for (Method m : companionClass.getMethods()) { + if ("getContaining".equals(m.getName()) && m.getParameterCount() == 2) { + Class[] params = m.getParameterTypes(); + if (Level.class.isAssignableFrom(params[0]) && Position.class.isAssignableFrom(params[1])) { + getContainingMethod = m; + } + } + } + + return (projectOutOfSubLevelMethod != null && getContainingMethod != null); + } + +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/TaskSwing.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/TaskSwing.java index ea78469d..94c9e7a2 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/TaskSwing.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/TaskSwing.java @@ -43,6 +43,7 @@ import org.vmstudio.visor.api.common.HandType; import org.vmstudio.visor.api.common.addon.VisorAddon; import org.vmstudio.visor.api.common.eventbus.event.VREvent; +import org.vmstudio.visor.compatibility.sable.SableCompatHelper; import org.vmstudio.visor.api.common.network.toserver.SwingAttackPayloadToServer; import org.vmstudio.visor.api.common.network.toserver.SwingBlockPayloadToServer; import org.vmstudio.visor.api.common.utils.VRMathUtils; @@ -659,7 +660,9 @@ private Vec3 restrictToFirstBlock(final Vec3 start, final Vec3 end) { ClipContext.Fluid.NONE, MC.player )); - return hitResult.getType() == HitResult.Type.BLOCK ? hitResult.getLocation() : end; + return hitResult.getType() == HitResult.Type.BLOCK + ? SableCompatHelper.isLoaded() ? SableCompatHelper.toWorldPos(MC.level, hitResult, hitResult.getLocation()) : hitResult.getLocation() + : end; } private void blockDust(Vec3 at, int count, BlockState state, diff --git a/visor-core/src/main/java/org/vmstudio/visor/mixin/client/renderer/GameRendererMixin.java b/visor-core/src/main/java/org/vmstudio/visor/mixin/client/renderer/GameRendererMixin.java index 6ef147f2..5e4120cc 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/mixin/client/renderer/GameRendererMixin.java +++ b/visor-core/src/main/java/org/vmstudio/visor/mixin/client/renderer/GameRendererMixin.java @@ -19,6 +19,7 @@ import org.vmstudio.visor.api.common.HandType; import org.vmstudio.visor.api.server.VRServerSettings; import org.vmstudio.visor.compatibility.immportals.ImmPortalsCompatHelper; +import org.vmstudio.visor.compatibility.sable.SableCompatHelper; import org.vmstudio.visor.core.client.VisorState; import org.vmstudio.visor.core.client.player.pose.LocalPlayerPose; import org.vmstudio.visor.core.client.tasks.types.movement.TaskTeleport; @@ -415,7 +416,15 @@ public abstract class GameRendererMixin HitResult hitResult = this.minecraft.hitResult; if (hitResult != null && hitResult.getType() != HitResult.Type.MISS) { // includes entity hits missed by visor$pickPos - this.visor$aimHitPos = hitResult.getLocation(); + if (SableCompatHelper.isLoaded()) { + this.visor$aimHitPos = SableCompatHelper.toWorldPos( + this.minecraft.level, + hitResult, + hitResult.getLocation() + ); + } else { + this.visor$aimHitPos = hitResult.getLocation(); + } } visor$handHitResult[hand.ordinal()] = hitResult; visor$handAimHitPos[hand.ordinal()] = this.visor$aimHitPos; @@ -552,7 +561,7 @@ public abstract class GameRendererMixin McVersionClientUtils.blockPickRange(this.minecraft.gameMode, this.minecraft.player) ); this.visor$aimHitPos = hitResult != null && hitResult.getType() != HitResult.Type.MISS - ? hitResult.getLocation() + ? SableCompatHelper.isLoaded() ? SableCompatHelper.toWorldPos(this.minecraft.level, hitResult, hitResult.getLocation()) : hitResult.getLocation() : fallbackAimHitPos; return new Vec3((Vector3f) renderPose.getHand(hand).getPosition()); @@ -1013,4 +1022,4 @@ this.minecraft.player, new PoseStack() return ImmPortalsCompatHelper.pickBlock(MC.level, vrPose, blockReachDistance, fluid, MC.player); } -} +} \ No newline at end of file diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java new file mode 100644 index 00000000..7100b538 --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java @@ -0,0 +1,16 @@ +package dev.ryanhcode.sable; + +import dev.ryanhcode.sable.companion.SableCompanion; +import dev.ryanhcode.sable.sublevel.SubLevel; + +public class ActiveSableCompanion implements SableCompanion { + @Override + public Object projectOutOfSubLevel(Object level, Object pos) { + return null; + } + + @Override + public SubLevel getContaining(Object level, Object pos) { + return null; + } +} diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/Sable.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/Sable.java new file mode 100644 index 00000000..c9de9b44 --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/Sable.java @@ -0,0 +1,10 @@ +package dev.ryanhcode.sable; + + +public final class Sable { + public static final String MOD_ID = "sable"; + public static final ActiveSableCompanion HELPER = null; + + private Sable() { + } +} diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/SableCompanion.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/SableCompanion.java new file mode 100644 index 00000000..5ead948a --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/SableCompanion.java @@ -0,0 +1,11 @@ +package dev.ryanhcode.sable.companion; + +import dev.ryanhcode.sable.sublevel.SubLevel; + +public interface SableCompanion { + SableCompanion INSTANCE = null; + + Object projectOutOfSubLevel(Object level, Object pos); + + SubLevel getContaining(Object level, Object pos); +} diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/math/Pose3dc.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/math/Pose3dc.java new file mode 100644 index 00000000..e1950165 --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/companion/math/Pose3dc.java @@ -0,0 +1,5 @@ +package dev.ryanhcode.sable.companion.math; + +public interface Pose3dc { + Object orientation(); +} diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/ClientSubLevel.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/ClientSubLevel.java new file mode 100644 index 00000000..db349158 --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/ClientSubLevel.java @@ -0,0 +1,9 @@ +package dev.ryanhcode.sable.sublevel; + +import dev.ryanhcode.sable.companion.math.Pose3dc; + +public class ClientSubLevel extends SubLevel { + public Pose3dc renderPose() { + return null; + } +} diff --git a/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/SubLevel.java b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/SubLevel.java new file mode 100644 index 00000000..c602aebd --- /dev/null +++ b/visor-core/stubs/src/main/java/dev/ryanhcode/sable/sublevel/SubLevel.java @@ -0,0 +1,9 @@ +package dev.ryanhcode.sable.sublevel; + +import dev.ryanhcode.sable.companion.math.Pose3dc; + +public class SubLevel { + public Pose3dc logicalPose() { + return null; + } +} From 75e8bde7912d375f9a1b60a9d7475f700dfdb37c Mon Sep 17 00:00:00 2001 From: timtaran Date: Thu, 10 Sep 2026 20:26:18 +0500 Subject: [PATCH 2/5] aeronautics physics staff compat --- .../aeronautics/AeronauticsHelper.java | 18 ++ .../aeronautics/CreativeStaffItemPose.java | 61 +++++++ .../mixin/PhysicsStaffClientHandlerMixin.java | 109 ++++++++++++ .../mixin/PhysicsStaffItemRendererMixin.java | 39 +++++ .../mixin/PhysicsStaffRenderHandlerMixin.java | 45 +++++ ...icsStaffServerHandlerDragSessionMixin.java | 162 ++++++++++++++++++ .../effects/hand/HandEffectCrosshair.java | 24 ++- .../core/common/addon/CoreAddonClient.java | 3 + .../resources/visor.aeronautics.mixins.json | 18 ++ .../src/main/resources/fabric.mod.json | 3 +- .../resources/META-INF/neoforge.mods.toml | 2 + 11 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/CreativeStaffItemPose.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffClientHandlerMixin.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffItemRendererMixin.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffRenderHandlerMixin.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffServerHandlerDragSessionMixin.java create mode 100644 visor-core/src/main/resources/visor.aeronautics.mixins.json diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java new file mode 100644 index 00000000..002aea8c --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java @@ -0,0 +1,18 @@ +package org.vmstudio.visor.compatibility.aeronautics; + +import org.jetbrains.annotations.NotNull; +import org.vmstudio.visor.api.ModLoader; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.common.addon.VisorAddon; + +public class AeronauticsHelper { + public static void initializeCompat(@NotNull VisorAddon owner) { + if (isLoaded()) { + VisorAPI.addonManager().getRegistries().itemPoses().registerComponent(new CreativeStaffItemPose(owner)); + } + } + + public static boolean isLoaded() { + return ModLoader.get().isModLoaded("simulated"); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/CreativeStaffItemPose.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/CreativeStaffItemPose.java new file mode 100644 index 00000000..c36d3927 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/CreativeStaffItemPose.java @@ -0,0 +1,61 @@ +package org.vmstudio.visor.compatibility.aeronautics; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.joml.Quaternionf; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.client.player.VRClientPlayer; +import org.vmstudio.visor.api.client.render.decoration.hand.VRHandItemPose; +import org.vmstudio.visor.api.common.HandType; +import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.api.common.addon.component.ComponentPriority; + +public class CreativeStaffItemPose extends VRHandItemPose { + private static final String ID = "creative_staff_pose"; + + public CreativeStaffItemPose(@NotNull VisorAddon owner) { super(owner); } + + @Override + public void applyPose(@NotNull PoseStack stack, + @NotNull AbstractClientPlayer player, + @NotNull HandType hand, + @NotNull ItemStack item, + float equipProgress, + float partialTicks) { + VRClientPlayer vrPlayer = VisorAPI.client().getVRPlayer(player.getUUID()); + if (vrPlayer == null) return; + + float scale = 1f; + float translateX = 0.0f; + float translateY = 0.4f; + float translateZ = -0.3f; + float yaw = -30f; + float pitch = 0f; + float roll = 0f; + + Quaternionf rotation = new Quaternionf(); + rotation.mul(Axis.ZP.rotationDegrees(roll)); + rotation.mul(Axis.YP.rotationDegrees(pitch)); + rotation.mul(Axis.XP.rotationDegrees(yaw)); + + stack.translate(translateX, translateY, translateZ); + stack.mulPose(rotation); + stack.scale(scale, scale, scale); + } + + @Override + public boolean canApplyPose(@NotNull AbstractClientPlayer player, + @NotNull HandType hand, + @NotNull ItemStack itemStack) { + return itemStack.getItem().toString().equals("simulated:creative_physics_staff"); + } + + @Override + public @NotNull ComponentPriority getPriority() { return ComponentPriority.NORMAL; } + + @Override + public @NotNull String getId() { return ID; } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffClientHandlerMixin.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffClientHandlerMixin.java new file mode 100644 index 00000000..c0953f9e --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffClientHandlerMixin.java @@ -0,0 +1,109 @@ +package org.vmstudio.visor.compatibility.aeronautics.mixin; + +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4fc; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.common.player.VRPlayer; +import org.vmstudio.visor.api.common.player.VRPlayerPose; +import org.vmstudio.visor.api.common.player.VRPose; +import org.vmstudio.visor.api.common.player.VisorPlayer; +import org.vmstudio.visor.compatibility.MixinGate; + +@Mixin(targets = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffClientHandler", remap = false) +@MixinGate(classes = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffClientHandler") +@Pseudo +public class PhysicsStaffClientHandlerMixin { + @Inject(method = "getStaffFocusPos", at = @At("HEAD"), cancellable = true) + private static void getStaffFocusPos(final Player player, final boolean mainHand, final float partialTicks, CallbackInfoReturnable cir) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = mainHand ? playerPose.getMainHand() : playerPose.getOffhand(); + + Matrix4fc rotation = handPose.getRotation(); + Vector3f offset = new Vector3f(0, 0.75f, -0.35f); + rotation.transformDirection(offset); + + cir.setReturnValue(handPose.getPositionVec3().add( + new Vec3(offset.x, offset.y, offset.z) + )); + } + } + + @Redirect( + method = "onItemUsed", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/player/LocalPlayer;pick(DFZ)Lnet/minecraft/world/phys/HitResult;" + ) + ) + private HitResult redirectPick(LocalPlayer player, double range, float tickDelta, boolean includeFluids) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + + Vec3 startPos = handPose.getPositionVec3(); + Vec3 endPos = startPos.add(handPose.getDirectionVec3().scale(range)); + + return player.level().clip(new ClipContext(startPos, endPos, ClipContext.Block.OUTLINE, includeFluids ? ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, player)); + } + + return player.pick(range, tickDelta, includeFluids); + } + + @Redirect( + method = "sendDraggingData", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getLookAngle()Lnet/minecraft/world/phys/Vec3;" + ) + ) + private Vec3 redirectLookAngle(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getDirectionVec3(); + } + + return player.getLookAngle(); + } + + @Redirect( + method = "startDraggingSubLevel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/player/LocalPlayer;getEyePosition()Lnet/minecraft/world/phys/Vec3;" + ) + ) + private Vec3 redirectStartDraggingEyePosition(LocalPlayer player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getPositionVec3(); + } + + return player.getEyePosition(); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffItemRendererMixin.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffItemRendererMixin.java new file mode 100644 index 00000000..5f355231 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffItemRendererMixin.java @@ -0,0 +1,39 @@ +package org.vmstudio.visor.compatibility.aeronautics.mixin; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.common.player.VRPlayer; +import org.vmstudio.visor.api.common.player.VRPlayerPose; +import org.vmstudio.visor.api.common.player.VRPose; +import org.vmstudio.visor.api.common.player.VisorPlayer; +import org.vmstudio.visor.compatibility.MixinGate; + +@Mixin(targets = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffItemRenderer", remap = false) +@MixinGate(classes = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffItemRenderer") +@Pseudo +public class PhysicsStaffItemRendererMixin { + @Redirect( + method = "render", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getEyePosition(F)Lnet/minecraft/world/phys/Vec3;" + ) + ) + private Vec3 redirectRenderEyePos(Player player, float partialTicks) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getPositionVec3(); + } + + return player.getEyePosition(partialTicks); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffRenderHandlerMixin.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffRenderHandlerMixin.java new file mode 100644 index 00000000..5964c59d --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffRenderHandlerMixin.java @@ -0,0 +1,45 @@ +package org.vmstudio.visor.compatibility.aeronautics.mixin; + +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.common.player.VRPlayer; +import org.vmstudio.visor.api.common.player.VRPlayerPose; +import org.vmstudio.visor.api.common.player.VRPose; +import org.vmstudio.visor.api.common.player.VisorPlayer; +import org.vmstudio.visor.compatibility.MixinGate; + +@Mixin(targets = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffRenderHandler", remap = false) +@MixinGate(classes = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffRenderHandler") +@Pseudo +public class PhysicsStaffRenderHandlerMixin { + @Redirect( + method = "updateHoverPos", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/player/LocalPlayer;pick(DFZ)Lnet/minecraft/world/phys/HitResult;" + ) + ) + private static HitResult redirectHoverPick(LocalPlayer player, double range, float tickDelta, boolean includeFluids) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + + Vec3 startPos = handPose.getPositionVec3(); + Vec3 endPos = startPos.add(handPose.getDirectionVec3().scale(range)); + + return player.level().clip(new ClipContext(startPos, endPos, ClipContext.Block.OUTLINE, includeFluids ? ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, player)); + } + + return player.pick(range, tickDelta, includeFluids); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffServerHandlerDragSessionMixin.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffServerHandlerDragSessionMixin.java new file mode 100644 index 00000000..41c60cbe --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/mixin/PhysicsStaffServerHandlerDragSessionMixin.java @@ -0,0 +1,162 @@ +package org.vmstudio.visor.compatibility.aeronautics.mixin; + +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.vmstudio.visor.api.VisorAPI; +import org.vmstudio.visor.api.common.player.VRPlayer; +import org.vmstudio.visor.api.common.player.VRPlayerPose; +import org.vmstudio.visor.api.common.player.VRPose; +import org.vmstudio.visor.api.common.player.VisorPlayer; +import org.vmstudio.visor.compatibility.MixinGate; + +import java.util.Objects; + +@Mixin(targets = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffServerHandler$DragSession", remap = false) +@MixinGate(classes = "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffServerHandler$DragSession") +@Pseudo +public class PhysicsStaffServerHandlerDragSessionMixin { + @Redirect( + method = "physicsTick", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/world/entity/player/Player;xOld:D" + ) + ) + private double redirectXOld(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + + VRPlayerPose playerPose = vrPlayer.getPoseHistoryTick().getEntry(1); + VRPose handPose; + handPose = Objects.requireNonNullElseGet(playerPose, vrPlayer::getPoseData).getHand(vrPlayer.getActiveHand()); + + return handPose.getPositionVec3().x(); + } + + return player.xOld; + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getX()D" + ) + ) + private double redirectGetX(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getPositionVec3().x(); + } + + return player.getX(); + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/world/entity/player/Player;yOld:D" + ) + ) + private double redirectYOld(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + + VRPlayerPose playerPose = vrPlayer.getPoseHistoryTick().getEntry(1); + VRPose handPose; + handPose = Objects.requireNonNullElseGet(playerPose, vrPlayer::getPoseData).getHand(vrPlayer.getActiveHand()); + + return handPose.getPositionVec3().y(); + } + + return player.yOld; + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getY()D" + ) + ) + private double redirectGetY(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getPositionVec3().y(); + } + + return player.getY(); + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getEyeHeight()F" + ) + ) + private float redirectGetEyeHeight(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + return 0.0f; + } + + return player.getEyeHeight(); + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/world/entity/player/Player;zOld:D" + ) + ) + private double redirectZOld(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + + VRPlayerPose playerPose = vrPlayer.getPoseHistoryTick().getEntry(1); + VRPose handPose; + handPose = Objects.requireNonNullElseGet(playerPose, vrPlayer::getPoseData).getHand(vrPlayer.getActiveHand()); + + return handPose.getPositionVec3().z(); + } + + return player.zOld; + } + + @Redirect( + method = "physicsTick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getZ()D" + ) + ) + private double redirectGetZ(Player player) { + VisorPlayer visorPlayer = VisorAPI.getVisorPlayer(player); + if (visorPlayer != null && visorPlayer.isVR()) { + VRPlayer vrPlayer = visorPlayer.asVR(); + VRPlayerPose playerPose = vrPlayer.getPoseData(); + + VRPose handPose = playerPose.getHand(vrPlayer.getActiveHand()); + return handPose.getPositionVec3().z(); + } + + return player.getZ(); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java index 206f77e4..d075833e 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java @@ -1,5 +1,6 @@ package org.vmstudio.visor.core.client.render.decoration.effects.hand; +import org.joml.*; import org.vmstudio.visor.api.compatibility.mcversion.render.McVertexBuilder; import org.vmstudio.visor.api.compatibility.mcversion.render.McRenderUtils; import com.mojang.blaze3d.platform.GlStateManager; @@ -16,10 +17,10 @@ import org.vmstudio.visor.api.common.addon.VisorAddon; import org.vmstudio.visor.api.server.VRServerSettings; import org.vmstudio.visor.compatibility.ShaderCompatHelper; +import org.vmstudio.visor.compatibility.sable.SableCompatHelper; import org.vmstudio.visor.core.client.ClientContext; import org.vmstudio.visor.extensions.client.render.GameRendererExtension; import org.vmstudio.visor.core.client.render.helpers.RenderPoseHelper; -import net.minecraft.client.gui.Gui; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; @@ -28,12 +29,10 @@ import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.NotNull; -import org.joml.AxisAngle4f; -import org.joml.Matrix4f; -import org.joml.Quaternionf; -import org.joml.Vector3f; import org.lwjgl.opengl.GL11C; +import java.lang.Math; + import static org.vmstudio.visor.core.client.VisorClientImpl.MC; @RegisterVRHandEffect @@ -153,6 +152,21 @@ private void applyCrossHairRotation(PoseStack poseStack, VRPlayerPoseClient pose, HitResult hit) { if (hit instanceof BlockHitResult bhr && bhr.getType() != HitResult.Type.MISS) { + if (SableCompatHelper.isLoaded()) { + Quaterniond subLevelOrientation = SableCompatHelper.getSubLevelOrientation(MC.level, hit.getLocation()); + if (subLevelOrientation != null) { + // todo: this kind of works (crosshair is not vertically aligned for rotated sublevels) + poseStack.mulPose( + new Quaternionf( + (float) subLevelOrientation.x, + (float) subLevelOrientation.y, + (float) subLevelOrientation.z, + (float) subLevelOrientation.w + ) + ); + } + } + switch (bhr.getDirection()) { case DOWN -> { rotateInDegrees(poseStack, pose.getHand(hand).getYawDegrees(), 0, 1, 0); diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/CoreAddonClient.java b/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/CoreAddonClient.java index f8e19e5e..103e150a 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/CoreAddonClient.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/CoreAddonClient.java @@ -4,6 +4,7 @@ import org.vmstudio.visor.api.client.gui.GuiTexture; import org.vmstudio.visor.api.common.HandType; import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.compatibility.aeronautics.AeronauticsHelper; import org.vmstudio.visor.core.client.ClientContext; import org.vmstudio.visor.core.client.gui.overlays.builtin.VROverlayFullscreenWarning; import org.vmstudio.visor.core.client.gui.overlays.builtin.VROverlayGameScreen; @@ -88,6 +89,8 @@ public void onAddonLoad() { ) ) ); + + AeronauticsHelper.initializeCompat(this); } diff --git a/visor-core/src/main/resources/visor.aeronautics.mixins.json b/visor-core/src/main/resources/visor.aeronautics.mixins.json new file mode 100644 index 00000000..bb9d9910 --- /dev/null +++ b/visor-core/src/main/resources/visor.aeronautics.mixins.json @@ -0,0 +1,18 @@ +{ + "required": true, + "package": "org.vmstudio.visor.compatibility.aeronautics.mixin", + "plugin": "org.vmstudio.visor.MixinConfig", + "compatibilityLevel": "JAVA_${mixin_compat_level}", + "refmap": "${mod_id}.refmap.json", + "client": [ + "PhysicsStaffClientHandlerMixin", + "PhysicsStaffItemRendererMixin", + "PhysicsStaffRenderHandlerMixin" + ], + "server": [ + ], + "minVersion": "0.8.4", + "mixins": [ + "PhysicsStaffServerHandlerDragSessionMixin" + ] +} diff --git a/visor-fabric/src/main/resources/fabric.mod.json b/visor-fabric/src/main/resources/fabric.mod.json index cee63e75..597b87f0 100644 --- a/visor-fabric/src/main/resources/fabric.mod.json +++ b/visor-fabric/src/main/resources/fabric.mod.json @@ -30,7 +30,8 @@ "visor.dynamicfps.mixins.json", "visor.iris.mixins.json", "visor.nvidium.mixins.json", - "visor.blur.mixins.json" + "visor.blur.mixins.json", + "visor.aeronautics.mixins.json" ], "depends": { "fabricloader": "${fabric_loader_version_range}", diff --git a/visor-neoforge/src/main/resources/META-INF/neoforge.mods.toml b/visor-neoforge/src/main/resources/META-INF/neoforge.mods.toml index 31a3e883..90123341 100644 --- a/visor-neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/visor-neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -30,6 +30,8 @@ config = "${mod_id}.iris.mixins.json" [[mixins]] config = "${mod_id}.blur.mixins.json" [[mixins]] +config = "${mod_id}.aeronautics.mixins.json" +[[mixins]] config = "${mod_id}.mixins.neoforge.json" [[dependencies.${mod_id}]] From f97a32056ce5ffcc3e6db719e682ec4eb398879b Mon Sep 17 00:00:00 2001 From: timtaran Date: Thu, 10 Sep 2026 20:46:14 +0500 Subject: [PATCH 3/5] fix: crosshair not vertically aligned on sublevels --- .../decoration/effects/hand/HandEffectCrosshair.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java index d075833e..cad6ae2c 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/render/decoration/effects/hand/HandEffectCrosshair.java @@ -151,11 +151,13 @@ private void applyCrossHairRotation(PoseStack poseStack, HandType hand, VRPlayerPoseClient pose, HitResult hit) { + float yaw = pose.getHand(hand).getYawDegrees(); + if (hit instanceof BlockHitResult bhr && bhr.getType() != HitResult.Type.MISS) { if (SableCompatHelper.isLoaded()) { Quaterniond subLevelOrientation = SableCompatHelper.getSubLevelOrientation(MC.level, hit.getLocation()); if (subLevelOrientation != null) { - // todo: this kind of works (crosshair is not vertically aligned for rotated sublevels) + yaw = 0; // otherwise vertical alignment on block would be broken poseStack.mulPose( new Quaternionf( (float) subLevelOrientation.x, @@ -169,11 +171,11 @@ private void applyCrossHairRotation(PoseStack poseStack, switch (bhr.getDirection()) { case DOWN -> { - rotateInDegrees(poseStack, pose.getHand(hand).getYawDegrees(), 0, 1, 0); + rotateInDegrees(poseStack, yaw, 0, 1, 0); rotateInDegrees(poseStack, -90, 1, 0, 0); } case UP -> { - rotateInDegrees(poseStack, -pose.getHand(hand).getYawDegrees(), 0, 1, 0); + rotateInDegrees(poseStack, -yaw, 0, 1, 0); rotateInDegrees(poseStack, 90, 1, 0, 0); } case WEST -> rotateInDegrees(poseStack, 90, 0, 1, 0); @@ -182,7 +184,7 @@ private void applyCrossHairRotation(PoseStack poseStack, default -> {} } } else { - rotateInDegrees(poseStack, -pose.getHand(hand).getYawDegrees(), 0, 1, 0); + rotateInDegrees(poseStack, -yaw, 0, 1, 0); rotateInDegrees(poseStack, -pose.getHand(hand).getPitchDegrees(), 1, 0, 0); } } From 0f6879d7388f03ba411b9e57af8642ef8ea1064a Mon Sep 17 00:00:00 2001 From: Bogdan <53151806+Phoenix-Ra@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:11:34 +0400 Subject: [PATCH 4/5] added new Visor Component: VRInputRedirect and used it in aeronautics compatibility --- .../redirect/RegisterVRInputRedirect.java | 29 ++++ .../input/redirect/VRInputRedirect.java | 94 ++++++++++++ .../api/common/addon/ComponentRegistries.java | 14 ++ .../aeronautics/AeroVRInputRedirect.java | 47 ++++++ .../aeronautics/AeronauticsHelper.java | 5 +- .../internal/AeronauticsInputInternal.java | 82 +++++++++++ .../core/client/input/VRInputManagerImpl.java | 8 +- .../actions/game/GameActionMovement.java | 9 ++ .../redirect/VRInputRedirectHandler.java | 139 ++++++++++++++++++ .../redirect/VRInputRedirectRegistry.java | 132 +++++++++++++++++ .../tasks/types/movement/TaskTeleport.java | 2 + .../common/addon/VisorRegistriesImpl.java | 6 + 12 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/RegisterVRInputRedirect.java create mode 100644 visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/VRInputRedirect.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroVRInputRedirect.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsInputInternal.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectHandler.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectRegistry.java diff --git a/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/RegisterVRInputRedirect.java b/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/RegisterVRInputRedirect.java new file mode 100644 index 00000000..fc74306a --- /dev/null +++ b/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/RegisterVRInputRedirect.java @@ -0,0 +1,29 @@ +package org.vmstudio.visor.api.client.input.redirect; + +import org.vmstudio.visor.api.common.addon.VisorAddon; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to register your {@link VRInputRedirect} automatically on addon load. + * + *

+ * Class have to:
+ * 1) Be a child of {@link VRInputRedirect}
+ * 2) Contain constructor with a single parameter: + * {@link VisorAddon} + *

+ * + *

+ * To make it detectable by Visor, you need to implement + * {@link VisorAddon#getAddonPackagePath()} + *

+ * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface RegisterVRInputRedirect { +} diff --git a/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/VRInputRedirect.java b/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/VRInputRedirect.java new file mode 100644 index 00000000..a77c1425 --- /dev/null +++ b/visor-api/src/main/java/org/vmstudio/visor/api/client/input/redirect/VRInputRedirect.java @@ -0,0 +1,94 @@ +package org.vmstudio.visor.api.client.input.redirect; + +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.player.LocalPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector2fc; +import org.vmstudio.visor.api.client.input.InputHelper; +import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.api.common.addon.component.ComponentPriority; +import org.vmstudio.visor.api.common.addon.component.PrioritySupporter; +import org.vmstudio.visor.api.common.addon.component.VisorComponent; + +/** + * Redirects VR movement input from joystick + * to methods in this component. + *

+ * Useful, when you need to add compatibility with mods that + * use mouse move/scroll in-game without a Screen + * (e.g. Create Aeronautics levers, physics assembler) + *

+ */ +public abstract class VRInputRedirect implements VisorComponent, PrioritySupporter { + + @Getter + private final VisorAddon owner; + + @Getter @Setter + private boolean enabled = true; + + public VRInputRedirect(@NotNull VisorAddon owner) { + this.owner = owner; + } + + + /** + * If VR input can be redirected + * + * @param player the local player + * @return true/false + */ + public abstract boolean canRedirect(@NotNull LocalPlayer player); + + /** + * The raw joystick state, delivered every VR pre-tick while the redirect is + * active. + * + *

+ * Use it for anything that follows the stick continuously - a lever + * being pulled, a wheel being turned. + *

+ * + * @param player the local player + * @param axis joystick position, x - right, y - forward, within [-1;1] + * @return true if the axis was used, which also skips + * {@link #onScroll(LocalPlayer, double, double)} for this tick + */ + public boolean onAxis(@NotNull LocalPlayer player, + @NotNull Vector2fc axis) { + return false; + } + + /** + * Discrete scroll steps built up from the vertical joystick axis. + * + * @param player the local player + * @param deltaX horizontal steps + * @param deltaY vertical steps + */ + public void onScroll(@NotNull LocalPlayer player, + double deltaX, + double deltaY) { + } + + /** + * Called once when the redirect stops being active. + * + * @param player the local player + */ + public void onStop(@Nullable LocalPlayer player) { + + } + + + public final boolean isEnabledAndCanRedirect(@NotNull LocalPlayer player) { + return enabled && canRedirect(player); + } + + @Override + public @NotNull ComponentPriority getPriority() { + return ComponentPriority.NORMAL; + } +} diff --git a/visor-api/src/main/java/org/vmstudio/visor/api/common/addon/ComponentRegistries.java b/visor-api/src/main/java/org/vmstudio/visor/api/common/addon/ComponentRegistries.java index 3799db7b..2abb1732 100644 --- a/visor-api/src/main/java/org/vmstudio/visor/api/common/addon/ComponentRegistries.java +++ b/visor-api/src/main/java/org/vmstudio/visor/api/common/addon/ComponentRegistries.java @@ -7,6 +7,8 @@ import org.vmstudio.visor.api.client.gui.settings.VRSettingsPreset; import org.vmstudio.visor.api.client.input.action.RegisterActionSet; import org.vmstudio.visor.api.client.input.action.VRActionSet; +import org.vmstudio.visor.api.client.input.redirect.RegisterVRInputRedirect; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; import org.vmstudio.visor.api.client.player.body.RegisterVRBodyType; import org.vmstudio.visor.api.client.player.body.VRBodyType; import org.vmstudio.visor.api.client.render.decoration.VRDecorator; @@ -53,6 +55,18 @@ public interface ComponentRegistries { ComponentRegistry actionSets(); + /** + * Get VR input redirect registry + * + *

Annotation to auto-register on load: {@link RegisterVRInputRedirect}

+ * + * @return VR Input redirect registry instance + */ + @NotNull + @Environment(EnvType.CLIENT) + ComponentRegistry inputRedirects(); + + /** * Get VR Decorator registry diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroVRInputRedirect.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroVRInputRedirect.java new file mode 100644 index 00000000..90faf181 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroVRInputRedirect.java @@ -0,0 +1,47 @@ +package org.vmstudio.visor.compatibility.aeronautics; + +import net.minecraft.client.player.LocalPlayer; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector2fc; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; +import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.compatibility.aeronautics.internal.AeronauticsInputInternal; + + +public class AeroVRInputRedirect extends VRInputRedirect { + private static final String ID = "aeronautics"; + + public AeroVRInputRedirect(@NotNull VisorAddon owner) { + super(owner); + } + + + @Override + public boolean canRedirect(@NotNull LocalPlayer player) { + return AeronauticsInputInternal.isHoldInteractionActive(); + } + + @Override + public boolean onAxis(@NotNull LocalPlayer player, + @NotNull Vector2fc axis) { + if (axis.x() == 0 && axis.y() == 0) { + return false; + } + return AeronauticsInputInternal.sendMouseMove( + axis.x() * 60.0, + -axis.y() * 15.0 + ); + } + + @Override + public void onScroll(@NotNull LocalPlayer player, + double deltaX, + double deltaY) { + AeronauticsInputInternal.sendMouseScroll(deltaX, deltaY); + } + + @Override + public @NotNull String getId() { + return ID; + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java index 002aea8c..b5aa39f8 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java @@ -8,7 +8,10 @@ public class AeronauticsHelper { public static void initializeCompat(@NotNull VisorAddon owner) { if (isLoaded()) { - VisorAPI.addonManager().getRegistries().itemPoses().registerComponent(new CreativeStaffItemPose(owner)); + var registries = VisorAPI.addonManager().getRegistries(); + + registries.itemPoses().registerComponent(new CreativeStaffItemPose(owner)); + registries.inputRedirects().registerComponent(new AeroVRInputRedirect(owner)); } } diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsInputInternal.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsInputInternal.java new file mode 100644 index 00000000..0fb924d1 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsInputInternal.java @@ -0,0 +1,82 @@ +package org.vmstudio.visor.compatibility.aeronautics.internal; + +import org.vmstudio.visor.api.common.utils.LoggerUtils; +import org.vmstudio.visor.compatibility.OneShotSetup; + +import java.lang.reflect.Method; + +public final class AeronauticsInputInternal { + private static final String HOLD_MANAGER_CLASS = + "dev.simulated_team.simulated.util.hold_interaction.HoldInteractionManager"; + private static final String CLIENT_EVENTS_CLASS = + "dev.simulated_team.simulated.events.SimulatedCommonClientEvents"; + private static final String RESULT_CLASS = + "dev.simulated_team.simulated.util.click_interactions.InteractCallback$Result"; + + private static final OneShotSetup SETUP = new OneShotSetup(AeronauticsInputInternal::resolve); + + private static Method isHoldActiveMethod; + private static Method onMouseMoveMethod; + private static Method onMouseScrollMethod; + private static Method resultCancelledMethod; + + + public static boolean isHoldInteractionActive() { + if (!SETUP.ok()) { + return false; + } + try { + return Boolean.TRUE.equals(isHoldActiveMethod.invoke(null)); + } catch (Throwable t) { + fail("read the Simulated hold interaction state", t); + return false; + } + } + + public static boolean sendMouseMove(double yaw, double pitch) { + return dispatch(onMouseMoveMethod, yaw, pitch, "mouse movement"); + } + public static boolean sendMouseScroll(double deltaX, double deltaY) { + return dispatch(onMouseScrollMethod, deltaX, deltaY, "mouse scroll"); + } + + + + + private static boolean dispatch(Method method, double x, double y, String what) { + if (!SETUP.ok()) { + return false; + } + try { + Object result = method.invoke(null, x, y); + return result != null + && Boolean.TRUE.equals(resultCancelledMethod.invoke(result)); + } catch (Throwable t) { + fail("send " + what + " to Simulated", t); + return false; + } + } + + private static void fail(String what, Throwable t) { + SETUP.disable(); + LoggerUtils.getLogger().warn("Visor: failed to {}, HUD input compat disabled", what, t); + } + + private static boolean resolve() throws ReflectiveOperationException { + isHoldActiveMethod = Class.forName(HOLD_MANAGER_CLASS) + .getMethod("isActive"); + + Class clientEvents = Class.forName(CLIENT_EVENTS_CLASS); + onMouseMoveMethod = clientEvents.getMethod("onMouseMove", double.class, double.class); + onMouseScrollMethod = clientEvents.getMethod("onMouseScroll", double.class, double.class); + + resultCancelledMethod = Class.forName(RESULT_CLASS).getMethod("cancelled"); + + return true; + } + + + private AeronauticsInputInternal() { + throw new UnsupportedOperationException("This is an utility class and cannot be instantiated"); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/VRInputManagerImpl.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/VRInputManagerImpl.java index ac41a471..d6ef4008 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/VRInputManagerImpl.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/VRInputManagerImpl.java @@ -18,6 +18,7 @@ import org.vmstudio.visor.core.client.input.actions.ActionMiddleMouse; import org.vmstudio.visor.core.client.input.actions.ActionRightMouse; import org.vmstudio.visor.core.client.input.actions.ActionScrollMouse; +import org.vmstudio.visor.core.client.input.redirect.VRInputRedirectRegistry; import org.vmstudio.visor.core.client.provider.openxr.XrProvider; import org.vmstudio.visor.api.client.settings.VRClientSettings; import org.jetbrains.annotations.NotNull; @@ -31,6 +32,9 @@ public class VRInputManagerImpl implements VRInputManager { @Getter private final ActionSetRegistry actionSetRegistry; + @Getter + private final VRInputRedirectRegistry inputRedirectRegistry; + @Getter private VRActionSet activeSet; @@ -40,6 +44,7 @@ public class VRInputManagerImpl implements VRInputManager { public VRInputManagerImpl(){ actionSetRegistry = new ActionSetRegistry(); + inputRedirectRegistry = new VRInputRedirectRegistry(); } @@ -172,7 +177,8 @@ public void triggerHapticPulse(@NotNull HandType hand, public List> getComponentRegistries(){ return List.of( - actionSetRegistry + actionSetRegistry, + inputRedirectRegistry ); } } diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/actions/game/GameActionMovement.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/actions/game/GameActionMovement.java index 97770da0..5b8b9659 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/actions/game/GameActionMovement.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/actions/game/GameActionMovement.java @@ -12,6 +12,7 @@ import org.vmstudio.visor.api.client.settings.VRClientSettings; import org.vmstudio.visor.api.client.settings.enums.MovementMode; import org.vmstudio.visor.core.client.input.TreadmillInput; +import org.vmstudio.visor.core.client.input.redirect.VRInputRedirectHandler; import org.vmstudio.visor.core.client.tasks.types.movement.TaskTeleport; import org.vmstudio.visor.core.client.utils.ClientUtils; import net.minecraft.util.Mth; @@ -54,6 +55,12 @@ public void preTick() { Vector2f movement = ClientContext.localPlayer.getMovement(); + if(VRInputRedirectHandler.INSTANCE.handle(MC.player, rawMove)){ + movement.zero(); + resetMovementState(); + return; + } + if(VRClientSettings.getMoveMode(MC.player) == MovementMode.TELEPORT){ resetMovementState(); movement.set(rawMove); @@ -158,6 +165,8 @@ protected void onStateChanged(@NotNull Vector2f newState) { @Override protected void onClear() { + VRInputRedirectHandler.INSTANCE.stop(); + Vector2f input = ClientContext.localPlayer.getMovement(); input.x = 0; diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectHandler.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectHandler.java new file mode 100644 index 00000000..e46db835 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectHandler.java @@ -0,0 +1,139 @@ +package org.vmstudio.visor.core.client.input.redirect; + +import net.minecraft.client.player.LocalPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector2f; +import org.joml.Vector2fc; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; +import org.vmstudio.visor.core.client.ClientContext; + +import static org.vmstudio.visor.core.client.VisorClientImpl.MC; + + +public class VRInputRedirectHandler { + public static final VRInputRedirectHandler INSTANCE = new VRInputRedirectHandler(); + + private static final float DEAD_ZONE = 0.1f; + private static final int REPEAT_DELAY_TICKS = 6; + private static final double MIN_STEPS_PER_SECOND = 3.0; + private static final double MAX_STEPS_PER_SECOND = 15.0; + + private final Vector2f axis = new Vector2f(); + + private VRInputRedirect active; + + private int heldDirection; + private int ticksHeld; + private double repeatSaved; + + + public boolean isRedirecting() { + return active != null; + } + + + public boolean handle(@Nullable LocalPlayer player, + @NotNull Vector2fc joystick) { + VRInputRedirect redirect = player == null ? null : findRedirect(player); + + if (redirect != active) { + stop(player); + active = redirect; + } + if (redirect == null || player == null) { + return false; + } + + axis.set( + applyDeadZone(joystick.x()), + applyDeadZone(joystick.y()) + ); + + if (redirect.onAxis(player, axis)) { + resetScroll(); + return true; + } + + scroll(player, redirect, axis.y); + return true; + } + + + public void stop(@Nullable LocalPlayer player) { + if (active == null) { + return; + } + VRInputRedirect previous = active; + active = null; + resetScroll(); + previous.onStop(player); + } + + public void stop() { + stop(MC.player); + } + + + public void onUnregistered(@NotNull VRInputRedirect component) { + if (active == component) { + stop(); + } + } + + + @Nullable + private VRInputRedirect findRedirect(@NotNull LocalPlayer player) { + for (VRInputRedirect entry : ClientContext.inputManager + .getInputRedirectRegistry().getSortedComponents()) { + if (entry.isEnabledAndCanRedirect(player)) { + return entry; + } + } + return null; + } + + private void scroll(@NotNull LocalPlayer player, + @NotNull VRInputRedirect redirect, + float value) { + if (value == 0) { + resetScroll(); + return; + } + + int direction = value > 0 ? 1 : -1; + if (direction != heldDirection) { + heldDirection = direction; + ticksHeld = 0; + repeatSaved = 0; + redirect.onScroll(player, 0, direction); + return; + } + + if (++ticksHeld < REPEAT_DELAY_TICKS) { + return; + } + + double stepsPerSecond = MIN_STEPS_PER_SECOND + + (MAX_STEPS_PER_SECOND - MIN_STEPS_PER_SECOND) * Math.abs(value); + + repeatSaved += stepsPerSecond / 20.0; + if (repeatSaved < 1) { + return; + } + // keep the remainder, so the repeat speed stays even + repeatSaved -= 1; + redirect.onScroll(player, 0, direction); + } + + private void resetScroll() { + heldDirection = 0; + ticksHeld = 0; + repeatSaved = 0; + } + + private float applyDeadZone(float value) { + float strength = Math.max(Math.abs(value) - DEAD_ZONE, 0F) / (1F - DEAD_ZONE); + return Math.copySign(strength, value); + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectRegistry.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectRegistry.java new file mode 100644 index 00000000..3f7f5ef7 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/input/redirect/VRInputRedirectRegistry.java @@ -0,0 +1,132 @@ +package org.vmstudio.visor.core.client.input.redirect; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.vmstudio.visor.api.ModLoader; +import org.vmstudio.visor.api.client.input.redirect.RegisterVRInputRedirect; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; +import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.api.common.addon.component.ComponentIds; +import org.vmstudio.visor.api.common.addon.component.ComponentRegistry; +import org.vmstudio.visor.api.common.utils.LoggerUtils; +import org.vmstudio.visor.core.client.VisorClientImpl; + +import java.lang.reflect.Constructor; +import java.util.*; + + +public class VRInputRedirectRegistry implements ComponentRegistry { + private static final String REGISTRY_NAME = "VR Input Redirects"; + + private static final String COMPONENT_NAME = "VRInputRedirect"; + private static final String ANNOTATION_NAME = "@RegisterVRInputRedirect"; + + @Getter + private final HashMap componentsMap = new HashMap<>(); + + private final List sortedComponents = new ArrayList<>(); + + @Getter + private final Collection allComponents = + Collections.unmodifiableCollection(componentsMap.values()); + + + public List getSortedComponents() { + return Collections.unmodifiableList(sortedComponents); + } + + + @Override + public void registerAddonPath(@NotNull VisorAddon addon) { + + String path = addon.getAddonPackagePath(); + if(path == null){ + return; + } + List> annotated = ModLoader.get().getClassesAnnotated( + RegisterVRInputRedirect.class, + addon.getModId(), + path + ); + + VisorClientImpl.LOGGER.info("Found {} {} to register in addon: '{}'", + annotated.size(), COMPONENT_NAME, addon.getAddonId()); + + for (Class clazz : annotated) { + if (!VRInputRedirect.class.isAssignableFrom(clazz)) { + VisorClientImpl.LOGGER.warn( + "{} is annotated with {} but does not implement {}", + clazz.getName(), ANNOTATION_NAME, COMPONENT_NAME + ); + continue; + } + try { + @SuppressWarnings("unchecked") + Constructor constructor = + ((Class) clazz) + .getConstructor(VisorAddon.class); + + var component = constructor.newInstance(addon); + + registerComponent(component); + + } catch (Exception e) { + VisorClientImpl.LOGGER.error("Failed to register {} from class: {}", COMPONENT_NAME, clazz.getName()); + LoggerUtils.printError(e); + } + } + } + + @Override + public void registerComponent(@NotNull VRInputRedirect component) { + String validationError = ComponentIds.validate(component.getId()); + if(validationError != null){ + throw new RuntimeException( + "Tried to register "+COMPONENT_NAME+" with ID '" + + component.getId() + + "'. From addon: '"+component.getOwner().getAddonId() + + "'. The ID pattern is incorrect: " + validationError); + } + + var previous = componentsMap.put(component.getId(), component); + + if (previous != null) { + VisorClientImpl.LOGGER.info( + "Overriding existing {}: '{}' from addon '{}'", + COMPONENT_NAME, + previous.getId(), + previous.getOwner().getAddonId() + ); + sortedComponents.remove(previous); + + }else{ + VisorClientImpl.LOGGER.info("Registered {}: '{}'", COMPONENT_NAME, component.getId()); + } + sortedComponents.add(component); + Collections.sort(sortedComponents); + } + + @Override + public VRInputRedirect unregisterComponent(@NotNull String id) { + var removed = componentsMap.remove(id); + if(removed != null) { + sortedComponents.remove(removed); + Collections.sort(sortedComponents); + VRInputRedirectHandler.INSTANCE.onUnregistered(removed); + VisorClientImpl.LOGGER.info("Unregistered {}: '{}'", COMPONENT_NAME, removed.getId()); + } + return removed; + } + + @Override + public @Nullable VRInputRedirect getComponent(@NotNull String id) { + return componentsMap.get(id); + } + + + @Override + public @NotNull String getRegistryName() { + return REGISTRY_NAME; + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/movement/TaskTeleport.java b/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/movement/TaskTeleport.java index c0f1db96..cb6f28b2 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/movement/TaskTeleport.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/client/tasks/types/movement/TaskTeleport.java @@ -15,6 +15,7 @@ import org.vmstudio.visor.api.common.utils.VRMathUtils; import org.vmstudio.visor.api.server.VRServerSettings; import org.vmstudio.visor.core.client.ClientContext; +import org.vmstudio.visor.core.client.input.redirect.VRInputRedirectHandler; import org.vmstudio.visor.api.client.settings.VRClientSettings; import org.vmstudio.visor.api.client.settings.enums.MovementMode; import org.vmstudio.visor.extensions.client.entity.LocalPlayerExtension; @@ -139,6 +140,7 @@ protected void onClear(@Nullable LocalPlayer player) { public boolean isActive(LocalPlayer player) { if (ClientContext.visor.isFeatureDisabled(ClientFeature.INPUT_MOVEMENT)) return false; if (VRClientSettings.getMoveMode(player) != MovementMode.TELEPORT) return false; + if (VRInputRedirectHandler.INSTANCE.isRedirecting()) return false; if(TaskRoomClimb.getInstance().isGrabbed()) return false; if (player == null || !player.isAlive() || player.isPassenger()) return false; return !player.isSleeping(); diff --git a/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/VisorRegistriesImpl.java b/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/VisorRegistriesImpl.java index 6d0357a3..6fcc03ac 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/VisorRegistriesImpl.java +++ b/visor-core/src/main/java/org/vmstudio/visor/core/common/addon/VisorRegistriesImpl.java @@ -4,6 +4,7 @@ import org.vmstudio.visor.api.client.gui.overlays.VROverlayTemplateRecord; import org.vmstudio.visor.api.client.gui.settings.VRSettingsPreset; import org.vmstudio.visor.api.client.input.action.VRActionSet; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; import org.vmstudio.visor.api.client.player.body.VRBodyType; import org.vmstudio.visor.api.client.render.decoration.VRDecorator; import org.vmstudio.visor.api.client.render.decoration.effects.VRGameEffect; @@ -34,6 +35,11 @@ public VisorRegistriesImpl(List> registries){ return ClientContext.inputManager.getActionSetRegistry(); } + @Override + public @NotNull ComponentRegistry inputRedirects() { + return ClientContext.inputManager.getInputRedirectRegistry(); + } + @Override public @NotNull ComponentRegistry decorators() { return ClientContext.decorationRenderer.getRegistry(); From 6609eb7fb7531322c26facbaf7ce227ef4523f4e Mon Sep 17 00:00:00 2001 From: Bogdan <53151806+Phoenix-Ra@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:32:33 +0400 Subject: [PATCH 5/5] physics staff dragging with VR input redirect --- .../aeronautics/AeroStaffVRInputRedirect.java | 51 ++++++++++++ .../aeronautics/AeronauticsHelper.java | 1 + .../internal/AeronauticsStaffInternal.java | 80 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroStaffVRInputRedirect.java create mode 100644 visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsStaffInternal.java diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroStaffVRInputRedirect.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroStaffVRInputRedirect.java new file mode 100644 index 00000000..db0af95c --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeroStaffVRInputRedirect.java @@ -0,0 +1,51 @@ +package org.vmstudio.visor.compatibility.aeronautics; + +import net.minecraft.client.player.LocalPlayer; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector2fc; +import org.vmstudio.visor.api.client.input.redirect.VRInputRedirect; +import org.vmstudio.visor.api.common.addon.VisorAddon; +import org.vmstudio.visor.compatibility.aeronautics.internal.AeronauticsInputInternal; +import org.vmstudio.visor.compatibility.aeronautics.internal.AeronauticsStaffInternal; + +public class AeroStaffVRInputRedirect extends VRInputRedirect { + private static final String ID = "aeronautics_staff"; + + + private static final double ROTATE_DEGREES_PER_TICK = 4.0; + + public AeroStaffVRInputRedirect(@NotNull VisorAddon owner) { + super(owner); + } + + + @Override + public boolean canRedirect(@NotNull LocalPlayer player) { + return player.isShiftKeyDown() && AeronauticsStaffInternal.isDragging(); + } + + @Override + public boolean onAxis(@NotNull LocalPlayer player, + @NotNull Vector2fc axis) { + if (axis.x() != 0) { + AeronauticsStaffInternal.rotateDragged( + Math.toRadians(axis.x() * ROTATE_DEGREES_PER_TICK) + ); + } + //We need scroll steps for the distance, so, + // don't send true here to not cancel scroll method + return false; + } + + @Override + public void onScroll(@NotNull LocalPlayer player, + double deltaX, + double deltaY) { + AeronauticsInputInternal.sendMouseScroll(deltaX, deltaY); + } + + @Override + public @NotNull String getId() { + return ID; + } +} diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java index b5aa39f8..06acf475 100644 --- a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java @@ -12,6 +12,7 @@ public static void initializeCompat(@NotNull VisorAddon owner) { registries.itemPoses().registerComponent(new CreativeStaffItemPose(owner)); registries.inputRedirects().registerComponent(new AeroVRInputRedirect(owner)); + registries.inputRedirects().registerComponent(new AeroStaffVRInputRedirect(owner)); } } diff --git a/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsStaffInternal.java b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsStaffInternal.java new file mode 100644 index 00000000..b4a67bc3 --- /dev/null +++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/internal/AeronauticsStaffInternal.java @@ -0,0 +1,80 @@ +package org.vmstudio.visor.compatibility.aeronautics.internal; + +import org.joml.Quaterniond; +import org.vmstudio.visor.api.common.utils.LoggerUtils; +import org.vmstudio.visor.compatibility.OneShotSetup; + +import java.lang.reflect.Method; + + +public final class AeronauticsStaffInternal { + private static final String CLIENT_CLASS = + "dev.simulated_team.simulated.SimulatedClient"; + private static final String STAFF_HANDLER_FIELD = "PHYSICS_STAFF_CLIENT_HANDLER"; + private static final String DRAG_SESSION_CLASS = + "dev.simulated_team.simulated.content.physics_staff.PhysicsStaffClientHandler$ClientDragSession"; + + private static final OneShotSetup SETUP = new OneShotSetup(AeronauticsStaffInternal::resolve); + + private static Object staffHandler; + private static Method getDragSessionMethod; + private static Method dragOrientationMethod; + + + + public static boolean isDragging() { + return getDragSession() != null; + } + + public static void rotateDragged(double radians) { + Object session = getDragSession(); + if (session == null) { + return; + } + try { + Object orientation = dragOrientationMethod.invoke(session); + if (orientation instanceof Quaterniond quaternion) { + quaternion.rotateLocalY(radians); + } + } catch (Throwable t) { + fail("rotate the physics staff drag session", t); + } + } + + + private static Object getDragSession() { + if (!SETUP.ok()) { + return null; + } + try { + return getDragSessionMethod.invoke(staffHandler); + } catch (Throwable t) { + fail("read the physics staff drag session", t); + return null; + } + } + + private static void fail(String what, Throwable t) { + SETUP.disable(); + LoggerUtils.getLogger().warn("Visor: failed to {}, physics staff input compat disabled", what, t); + } + + private static boolean resolve() throws ReflectiveOperationException { + staffHandler = Class.forName(CLIENT_CLASS) + .getField(STAFF_HANDLER_FIELD) + .get(null); + if (staffHandler == null) { + return false; + } + + getDragSessionMethod = staffHandler.getClass().getMethod("getDragSession"); + dragOrientationMethod = Class.forName(DRAG_SESSION_CLASS).getMethod("dragOrientation"); + + return true; + } + + + private AeronauticsStaffInternal() { + throw new UnsupportedOperationException("This is an utility class and cannot be instantiated"); + } +}