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/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/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
new file mode 100644
index 00000000..06acf475
--- /dev/null
+++ b/visor-core/src/main/java/org/vmstudio/visor/compatibility/aeronautics/AeronauticsHelper.java
@@ -0,0 +1,22 @@
+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()) {
+ var registries = VisorAPI.addonManager().getRegistries();
+
+ registries.itemPoses().registerComponent(new CreativeStaffItemPose(owner));
+ registries.inputRedirects().registerComponent(new AeroVRInputRedirect(owner));
+ registries.inputRedirects().registerComponent(new AeroStaffVRInputRedirect(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/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/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");
+ }
+}
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/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/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 extends VRInputRedirect> constructor =
+ ((Class extends VRInputRedirect>) 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/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..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
@@ -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
@@ -152,14 +151,31 @@ 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) {
+ yaw = 0; // otherwise vertical alignment on block would be broken
+ 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);
+ 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);
@@ -168,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);
}
}
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/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/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/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();
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/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-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;
+ }
+}
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}]]