diff --git a/build.gradle b/build.gradle index 6490cb6..e3ec25c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "architectury-plugin" version "3.4-SNAPSHOT" - id "dev.architectury.loom" version "1.7-SNAPSHOT" apply false + id "dev.architectury.loom" version "1.9-SNAPSHOT" apply false } architectury { diff --git a/common/src/main/java/com/specialeffect/eyemine/mixin/MinecraftAccessor.java b/common/src/main/java/com/specialeffect/eyemine/mixin/MinecraftAccessor.java index 464d52a..3a0b0b9 100644 --- a/common/src/main/java/com/specialeffect/eyemine/mixin/MinecraftAccessor.java +++ b/common/src/main/java/com/specialeffect/eyemine/mixin/MinecraftAccessor.java @@ -8,4 +8,10 @@ public interface MinecraftAccessor { @Accessor("fps") int getFPS(); + + @Accessor("missTime") + void setMissTime(int missTime); + + @Accessor("missTime") + int getMissTime(); } diff --git a/common/src/main/java/com/specialeffect/eyemine/mixin/MouseHandlerMixin.java b/common/src/main/java/com/specialeffect/eyemine/mixin/MouseHandlerMixin.java index 89f3227..506cd70 100644 --- a/common/src/main/java/com/specialeffect/eyemine/mixin/MouseHandlerMixin.java +++ b/common/src/main/java/com/specialeffect/eyemine/mixin/MouseHandlerMixin.java @@ -11,10 +11,17 @@ package com.specialeffect.eyemine.mixin; +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.InputConstants; +import com.specialeffect.eyemine.EyeMine; +import com.specialeffect.eyemine.platform.EyeMineConfig; import com.specialeffect.eyemine.utils.MouseHelper; +import com.specialeffect.eyemine.utils.MouseHelper.PlayerMovement; import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.util.SmoothDouble; +import org.lwjgl.glfw.GLFW; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -22,6 +29,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(MouseHandler.class) public abstract class MouseHandlerMixin { @@ -44,18 +52,17 @@ public abstract class MouseHandlerMixin { @Shadow private boolean mouseGrabbed; - @Shadow - protected abstract void turnPlayer(double d); - @Shadow private double lastHandleMovementTime; @Shadow @Final private SmoothDouble smoothTurnX; + @Shadow @Final private SmoothDouble smoothTurnY; + @Shadow private boolean ignoreFirstMove; @@ -65,277 +72,381 @@ public abstract class MouseHandlerMixin { @Shadow public abstract boolean isMouseGrabbed(); - @Shadow public abstract void setIgnoreFirstMove(); + @Shadow + protected abstract void turnPlayer(double d); @Unique public float eyemine$deadBorder = 0.05f; + @Unique public float eyemine$clipBorderHorizontal = 0.3f; + @Unique public float eyemine$clipBorderVertical = 0.2f; + // Store last known cursor position for ungrabbed mode + @Unique + private double eyemine$lastX = 0; + @Unique + private double eyemine$lastY = 0; + + /** + * Inject at HEAD of onMove to add our pending event tracking + */ @Inject(method = "onMove(JDD)V", at = @At(value = "HEAD")) - public void EyeMine$addPendingEvent(long windowPointer, double xPos, double yPos, CallbackInfo ci) { + public void eyemine$addPendingEvent(long windowPointer, double xPos, double yPos, CallbackInfo ci) { MouseHelper.addPendingEvent(); } -// @Inject(method = "onMove(JDD)V", -// locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At( -// value = "INVOKE", -// target = "Lnet/minecraft/client/Minecraft;isWindowActive()Z", -// shift = At.Shift.BEFORE, -// ordinal = 0)) -// public void eyemine$GrabMouseOnMove(CallbackInfo ci) { -// // If mouse should be grabbed but isn't - this can happen if we alt-tab -// // away while world is loading, with pauseOnLostFocus=false -// if (this.minecraft.level != null && !MouseHelper.ungrabbedMouseMode && this.minecraft.isWindowActive() && !this.isMouseGrabbed()) { -// this.grabMouse(); -// } -// } -// -// @Inject(method = "handleAccumulatedMovement()V", -// locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At( -// value = "INVOKE", -// target = "Lnet/minecraft/client/Minecraft;getWindow()Lcom/mojang/blaze3d/platform/Window;", -// shift = At.Shift.BEFORE, -// ordinal = 0), cancellable = true) -// public void eyemineSetInputMode(CallbackInfo ci) { -// GLFW.glfwSetInputMode(this.minecraft.getWindow().getWindow(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); -// } -// -// -// @Inject(method = "handleAccumulatedMovement()V", -// locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At( -// value = "INVOKE", -// target = "Lnet/minecraft/client/MouseHandler;isMouseGrabbed()Z", -// shift = At.Shift.BEFORE, -// ordinal = 0), cancellable = true) -// public void eyemine$ProcessOnMove(CallbackInfo ci) { -// if (this.minecraft.isWindowActive()) { -// this.architectury_eyemine$processMousePosition(xpos, ypos); -// } -// } -// -// @Inject(method = "turnPlayer(D)V", -// locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At( -// value = "RETURN"), cancellable = true) -// public void eyemine$GrabMouseOnMove3(CallbackInfo ci) { -// // Reset to centre -// if (!MouseHelper.ungrabbedMouseMode) { -// GLFW.glfwSetCursorPos(this.minecraft.getWindow().getWindow(), 0, 0); -// this.xpos = 0; -// this.ypos = 0; -// } -// ci.cancel(); -// } -// -// @Unique -// private void architectury_eyemine$processMousePosition(double x, double y) { -// double w_half = (double) this.minecraft.getWindow().getScreenWidth() / 2; -// double h_half = (double) this.minecraft.getWindow().getScreenHeight() / 2; -// -// // adjust coordinates to centralised when ungrabbed -// if (MouseHelper.ungrabbedMouseMode) { -// x -= w_half; -// y -= h_half; -// } -// -// double x_abs = Math.abs(x); -// double y_abs = Math.abs(y); -// -// double deltaX = 0; -// double deltaY = 0; -// -// // If mouse is outside minecraft window, throw it away -// if (x_abs > w_half * (1 - this.eyemine$deadBorder) || -// y_abs > h_half * (1 - this.eyemine$deadBorder)) { -// // do nothing -// this.architectury_eyemine$resetVelocity(); -// } else { -// // If mouse is around edges, clip effect -// if (x_abs > w_half * (1 - this.eyemine$clipBorderHorizontal)) { -// x = (int) (Math.signum(x) * (w_half * (1 - this.eyemine$clipBorderHorizontal))); -// } -// if (y_abs > h_half * (1 - this.eyemine$clipBorderVertical)) { -// y = (int) (Math.signum(y) * (h_half * (1 - this.eyemine$clipBorderVertical))); -// } -// deltaX = x; -// deltaY = y; -// -// this.accumulatedDX = deltaX; -// this.accumulatedDY = deltaY; -// -// // Remember there was a valid event, even if we're not moving -// MouseHelper.mHasPendingEvent = true; -// } -// } -// -// @Unique -// private void architectury_eyemine$resetVelocity() { -// MouseHelper.lastXVelocity = this.accumulatedDX; -// MouseHelper.lastYVelocity = this.accumulatedDY; -// this.accumulatedDX = 0.0D; -// this.accumulatedDY = 0.0D; -// } -// -// @Inject(method = "turnPlayer(D)V", at = @At(value = "HEAD"), cancellable = true) -// public void EyeMineTurnPlayer(double movementTime, CallbackInfo ci) { -// // this gets called from Minecraft itself -// if (this.minecraft.player == null) { -// ci.cancel(); -// } -// -// if (MouseHelper.movementState == MouseHelper.PlayerMovement.VANILLA) { -// architectury_eyemine$updatePlayerLookVanilla(movementTime); -// } else if (MouseHelper.movementState == MouseHelper.PlayerMovement.LEGACY) { -// architectury_eyemine$updatePlayerLookLegacy(movementTime); -// } else { -// // keep track of last time -// double d0 = Blaze3D.getTime(); -// this.lastHandleMovementTime = d0; -// } -// -// ci.cancel(); -// } -// -// @Unique -// public void architectury_eyemine$updatePlayerLookVanilla(double movementTime) { -// double sensitivity = (Double) this.minecraft.options.sensitivity().get() * 0.6000000238418579 + 0.20000000298023224; -// double f = sensitivity * sensitivity * sensitivity; -// double g = f * 8.0; -// double j; -// double k; -// if (this.minecraft.options.smoothCamera) { -// double h = this.smoothTurnX.getNewDeltaValue(this.accumulatedDX * g, movementTime * g); -// double i = this.smoothTurnY.getNewDeltaValue(this.accumulatedDY * g, movementTime * g); -// j = h; -// k = i; -// } else if (this.minecraft.options.getCameraType().isFirstPerson() && this.minecraft.player.isScoping()) { -// this.smoothTurnX.reset(); -// this.smoothTurnY.reset(); -// j = this.accumulatedDX * f; -// k = this.accumulatedDY * f; -// } else { -// this.smoothTurnX.reset(); -// this.smoothTurnY.reset(); -// j = this.accumulatedDX * g; -// k = this.accumulatedDY * g; -// } -// -// int l = 1; -// if (this.minecraft.options.invertYMouse().get()) { -// l = -1; -// } -// -// this.minecraft.getTutorial().onMouse(j, k); -// if (this.minecraft.player != null) { -// this.minecraft.player.turn(j, k * (double) l); -// } -// } -// -// //TODO: Check if this is correct or if it needs to be updated -// @Unique -// public void architectury_eyemine$updatePlayerLookLegacy(double movementTime) { -// // Rotate the player (yaw) according to x position only -// double d0 = Blaze3D.getTime(); -// this.lastHandleMovementTime = d0; -// if (this.minecraft.isWindowActive()) { -// double d4 = 0.1 * this.minecraft.options.sensitivity().get() * (double) 0.6F + (double) 0.2F; -// double d5 = 0.5d * d4 * d4 * d4 * 8.0D; -// double d2; -// if (this.minecraft.options.smoothCamera) { -// double d6 = this.smoothTurnX.getNewDeltaValue(this.accumulatedDX * d5, movementTime * d5); -// d2 = d6; -// } else { -// this.smoothTurnX.reset(); -// this.smoothTurnY.reset(); -// -// // quadratic fit near centre -// double w = this.minecraft.getWindow().getGuiScaledWidth(); -// double h = this.minecraft.getWindow().getGuiScaledHeight(); -// double d = h / 8; -// double p = 2; // quadratic near centre -// double k = 2; // magnitude at inflection point -// k = 0.5 * (d5 * w) / (1 + p * (w / (2 * d) - 1)); // adjust k so effect at edge is same as with linear version -// -// // linear further out (but continuous at transition point) -// double a = k * (1 - p); -// double m = p * k / d; -// -// if (Math.abs(this.accumulatedDX) > d) { -// d2 = Math.signum(this.accumulatedDX) * (a + m * Math.abs(this.accumulatedDX)); -// } else { -// d2 = Math.signum(this.accumulatedDX) * k * Math.pow(Math.abs(this.accumulatedDX) / d, p); -// } -// -// // When going backward, reduce the yaw effect -// // TODO: ideally we'd have some smoother modulation here -// double h6 = (double) this.minecraft.getWindow().getGuiScaledHeight() / 6; -// if (this.accumulatedDY > h6) { -// d2 *= 0.5; -// } -// } -// -// this.architectury_eyemine$resetVelocity(); -// -// this.minecraft.getTutorial().onMouse(d2, 0); -// if (this.minecraft.screen == null) { -// if (this.minecraft.player != null) { -// this.minecraft.player.turn(d2, 0); -// } -// } -// -// // TODO: use the y position to walk forward/back too: or does this happen in WalkWithGaze2 mod? -// } else { -// this.architectury_eyemine$resetVelocity(); -// } -// } -// -// /** -// * Returns true if the mouse is grabbed. -// */ -// @Inject(method = "isMouseGrabbed()Z", at = @At("RETURN"), cancellable = true) -// public void EyeMine$IsMouseGrabbed(CallbackInfoReturnable cir) { -// // Somewhere deep in the MC engine, this is being queried to see whether mining should -// // occur, so we have to lie a little. -// boolean flag = this.mouseGrabbed; -// boolean flag2 = (this.minecraft.isWindowActive() && MouseHelper.ungrabbedMouseMode); -// cir.setReturnValue(flag || flag2); -// } -// -// @Inject(method = "grabMouse()V", at = @At(value = "HEAD"), cancellable = true) -// public void EyeMine$GrabMouse(CallbackInfo ci) { -// EyeMine.LOGGER.info("grabMouse"); -// if (!MouseHelper.hasGLcontext()) { -// EyeMine.LOGGER.info("grabMouse: no GL context"); -// ci.cancel(); -// } -// } -// -// @Inject(method = "grabMouse()V", -// locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At( -// value = "INVOKE", -// target = "Lcom/mojang/blaze3d/platform/InputConstants;grabOrReleaseMouse(JIDD)V", -// shift = At.Shift.BEFORE, -// ordinal = 0), cancellable = true) -// public void EyeMine$OnlyGrabWhenUngrabbed(CallbackInfo ci) { -// if (!MouseHelper.ungrabbedMouseMode) { -// InputConstants.grabOrReleaseMouse(this.minecraft.getWindow().getWindow(), 212995, this.xpos, this.ypos); -// } -// this.minecraft.setScreen((Screen) null); -// this.minecraft.missTime = 10000; -// this.ignoreFirstMove = true; -// ci.cancel(); -// } -// -// /** -// * Resets the player keystate, disables the ingame focus, and ungrabs the mouse cursor. -// */ -// @Inject(at = @At("HEAD"), method = "releaseMouse()V", cancellable = true) -// public void EyeMine$ReleaseMouse(CallbackInfo ci) { -// if (!MouseHelper.hasGLcontext()) { -// ci.cancel(); -// } -// } + /** + * Set cursor to normal mode before vanilla processes - this allows us to read position + */ + @Inject(method = "onMove(JDD)V", at = @At(value = "FIELD", + target = "Lnet/minecraft/client/MouseHandler;minecraft:Lnet/minecraft/client/Minecraft;", + ordinal = 0)) + public void eyemine$setInputMode(long handle, double xpos, double ypos, CallbackInfo ci) { + GLFW.glfwSetInputMode(this.minecraft.getWindow().getWindow(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + } + + /** + * Main injection point - replaces vanilla onMove logic after the window handle check + */ + @Inject(method = "onMove(JDD)V", at = @At(value = "FIELD", + target = "Lnet/minecraft/client/MouseHandler;ignoreFirstMove:Z", + ordinal = 0), cancellable = true) + public void eyemine$processOnMove(long handle, double xpos, double ypos, CallbackInfo ci) { + // Check if we're on a screen - if so, just update position and let vanilla handle it + if (this.minecraft.screen != null && this.minecraft.getOverlay() == null) { + this.xpos = xpos; + this.ypos = ypos; + ci.cancel(); + return; + } + + // If mouse should be grabbed but isn't - this can happen if we alt-tab + // away while world is loading, with pauseOnLostFocus=false + if (!MouseHelper.ungrabbedMouseMode && this.minecraft.isWindowActive() && !this.mouseGrabbed) { + this.grabMouse(); + } + + // Store current position for ungrabbed mode + if (MouseHelper.ungrabbedMouseMode) { + this.eyemine$lastX = xpos; + this.eyemine$lastY = ypos; + } + + // Check if gaze is below the hotbar area (configurable threshold) + // This is where the EyeMine keyboard renders, so walking should pause + // Skip the check if position is (0,0) in grabbed mode - that's our cursor reset position, + // not a real gaze. GLFW fires an onMove callback when we reset the cursor. + int threshold = EyeMineConfig.getGazeIdleThreshold(); + boolean isCursorResetEvent = !MouseHelper.ungrabbedMouseMode && xpos == 0 && ypos == 0; + if (threshold > 0 && !isCursorResetEvent) { + double screenHeight = this.minecraft.getWindow().getScreenHeight(); + // Use raw ypos from callback - this is the actual gaze/cursor position + // In grabbed mode, ypos is reported relative to window after we set NORMAL mode above + double thresholdY = screenHeight * (1.0 - threshold / 100.0); + boolean wasBelow = MouseHelper.isGazeBelowHotbar; + MouseHelper.isGazeBelowHotbar = ypos > thresholdY; + // Log when state changes to help debug + if (MouseHelper.isGazeBelowHotbar != wasBelow) { + EyeMine.LOGGER.info("Gaze threshold: ypos={}, screenHeight={}, threshold={}%, thresholdY={}, isBelow={}", + String.format("%.1f", ypos), String.format("%.1f", screenHeight), threshold, + String.format("%.1f", thresholdY), MouseHelper.isGazeBelowHotbar); + } + } else if (threshold <= 0) { + MouseHelper.isGazeBelowHotbar = false; + } + // Note: if isCursorResetEvent, we leave isGazeBelowHotbar unchanged + + // Process mouse position - sets accumulatedDX/DY based on position + if (this.minecraft.isWindowActive()) { + this.eyemine$processMousePosition(xpos, ypos, isCursorResetEvent); + } + + // In grabbed mode (eye tracker), reset cursor to origin after each move + // This makes each subsequent position effectively a delta + if (!MouseHelper.ungrabbedMouseMode) { + GLFW.glfwSetCursorPos(this.minecraft.getWindow().getWindow(), 0, 0); + this.xpos = 0; + this.ypos = 0; + // In grabbed mode, call turnPlayer immediately since position IS the delta + this.turnPlayer(Blaze3D.getTime() - this.lastHandleMovementTime); + } else { + // In ungrabbed mode, update xpos/ypos for vanilla compatibility + this.xpos = xpos; + this.ypos = ypos; + } + + ci.cancel(); + } + + @Unique + private void eyemine$processMousePosition(double x, double y, boolean isCursorResetEvent) { + double w_half = (double) this.minecraft.getWindow().getScreenWidth() / 2; + double h_half = (double) this.minecraft.getWindow().getScreenHeight() / 2; + + // Adjust coordinates to centralized when ungrabbed + if (MouseHelper.ungrabbedMouseMode) { + x -= w_half; + y -= h_half; + } + + double x_abs = Math.abs(x); + double y_abs = Math.abs(y); + + // Check if gaze is outside the window (in dead border) + // Skip updating for cursor reset events - (0,0) is our reset position, not real gaze + boolean isOutside = x_abs > w_half * (1 - this.eyemine$deadBorder) || + y_abs > h_half * (1 - this.eyemine$deadBorder); + if (!isCursorResetEvent) { + MouseHelper.isGazeOutsideWindow = isOutside; + } + + // If mouse is outside minecraft window, or gaze is at keyboard area, stop camera movement + if (isOutside || MouseHelper.isGazeBelowHotbar) { + // do nothing + this.eyemine$resetVelocity(); + } else { + // If mouse is around edges, clip effect + if (x_abs > w_half * (1 - this.eyemine$clipBorderHorizontal)) { + x = (int) (Math.signum(x) * (w_half * (1 - this.eyemine$clipBorderHorizontal))); + } + if (y_abs > h_half * (1 - this.eyemine$clipBorderVertical)) { + y = (int) (Math.signum(y) * (h_half * (1 - this.eyemine$clipBorderVertical))); + } + + this.accumulatedDX = x; + this.accumulatedDY = y; + + // Remember there was a valid event, even if we're not moving + MouseHelper.mHasPendingEvent = true; + } + } + + @Unique + private void eyemine$resetVelocity() { + MouseHelper.lastXVelocity = this.accumulatedDX; + MouseHelper.lastYVelocity = this.accumulatedDY; + this.accumulatedDX = 0.0D; + this.accumulatedDY = 0.0D; + } + + /** + * Override turnPlayer to use EyeMine's custom look logic + */ + @Inject(method = "turnPlayer(D)V", at = @At(value = "HEAD"), cancellable = true) + public void eyemine$turnPlayer(double movementTime, CallbackInfo ci) { + if (this.minecraft.player == null) { + ci.cancel(); + return; + } + + // In ungrabbed mode (emulation), recalculate accumulated values from stored position + // This ensures values are correct even when called from game loop (handleAccumulatedMovement) + if (MouseHelper.ungrabbedMouseMode) { + this.eyemine$recalculateFromStoredPosition(); + } + + if (MouseHelper.movementState == PlayerMovement.VANILLA) { + eyemine$updatePlayerLookVanilla(); + } else if (MouseHelper.movementState == PlayerMovement.LEGACY) { + eyemine$updatePlayerLookLegacy(); + } else { + // NONE state - keep track of last time but don't turn + this.lastHandleMovementTime = Blaze3D.getTime(); + } + + ci.cancel(); + } + + /** + * Recalculate accumulatedDX/DY from stored cursor position for ungrabbed mode + */ + @Unique + private void eyemine$recalculateFromStoredPosition() { + double w_half = (double) this.minecraft.getWindow().getScreenWidth() / 2; + double h_half = (double) this.minecraft.getWindow().getScreenHeight() / 2; + + // Position relative to center + double x = this.eyemine$lastX - w_half; + double y = this.eyemine$lastY - h_half; + + double x_abs = Math.abs(x); + double y_abs = Math.abs(y); + + // If mouse is outside dead border, don't turn + if (x_abs > w_half * (1 - this.eyemine$deadBorder) || + y_abs > h_half * (1 - this.eyemine$deadBorder)) { + this.accumulatedDX = 0; + this.accumulatedDY = 0; + } else { + // Clip to border regions + if (x_abs > w_half * (1 - this.eyemine$clipBorderHorizontal)) { + x = Math.signum(x) * (w_half * (1 - this.eyemine$clipBorderHorizontal)); + } + if (y_abs > h_half * (1 - this.eyemine$clipBorderVertical)) { + y = Math.signum(y) * (h_half * (1 - this.eyemine$clipBorderVertical)); + } + + this.accumulatedDX = x; + this.accumulatedDY = y; + } + } + + @Unique + public void eyemine$updatePlayerLookVanilla() { + double d0 = Blaze3D.getTime(); + double d1 = d0 - this.lastHandleMovementTime; + this.lastHandleMovementTime = d0; + + if (this.minecraft.isWindowActive()) { + double d4 = this.minecraft.options.sensitivity().get() * 0.6000000238418579D + 0.20000000298023224D; + double d5 = d4 * d4 * d4 * 8.0D; + double d2; + double d3; + + if (this.minecraft.options.smoothCamera) { + double d6 = this.smoothTurnX.getNewDeltaValue(this.accumulatedDX * d5, d1 * d5); + double d7 = this.smoothTurnY.getNewDeltaValue(this.accumulatedDY * d5, d1 * d5); + d2 = d6; + d3 = d7; + } else { + this.smoothTurnX.reset(); + this.smoothTurnY.reset(); + + // quadratic fit near centre + double w = this.minecraft.getWindow().getGuiScaledWidth(); + double h = this.minecraft.getWindow().getGuiScaledHeight(); + double d = h / 8; + + double p = 2; // quadratic near centre + double k = 0.5 * (d5 * w) / (1 + p * (w / (2 * d) - 1)); + + // linear further out (but continuous at transition point) + double a = k * (1 - p); + double m = p * k / d; + + if (Math.abs(this.accumulatedDX) > d) { + d2 = Math.signum(this.accumulatedDX) * (a + m * Math.abs(this.accumulatedDX)); + } else { + d2 = Math.signum(this.accumulatedDX) * k * Math.pow(Math.abs(this.accumulatedDX) / d, p); + } + if (Math.abs(this.accumulatedDY) > d) { + d3 = Math.signum(this.accumulatedDY) * (a + m * Math.abs(this.accumulatedDY)); + } else { + d3 = Math.signum(this.accumulatedDY) * k * Math.pow(Math.abs(this.accumulatedDY) / d, p); + } + } + + this.eyemine$resetVelocity(); + int i = this.minecraft.options.invertYMouse().get() ? -1 : 1; + + this.minecraft.getTutorial().onMouse(d2, d3); + if (this.minecraft.player != null) { + this.minecraft.player.turn(d2, d3 * (double) i); + } + } else { + this.eyemine$resetVelocity(); + } + } + + @Unique + public void eyemine$updatePlayerLookLegacy() { + // Rotate the player (yaw) according to x position only + double d0 = Blaze3D.getTime(); + double d1 = d0 - this.lastHandleMovementTime; + this.lastHandleMovementTime = d0; + + if (this.minecraft.isWindowActive()) { + double d4 = 0.1 * this.minecraft.options.sensitivity().get() * 0.6F + 0.2F; + double d5 = 0.5d * d4 * d4 * d4 * 8.0D; + double d2; + + if (this.minecraft.options.smoothCamera) { + d2 = this.smoothTurnX.getNewDeltaValue(this.accumulatedDX * d5, d1 * d5); + } else { + this.smoothTurnX.reset(); + this.smoothTurnY.reset(); + + // quadratic fit near centre + double w = this.minecraft.getWindow().getGuiScaledWidth(); + double h = this.minecraft.getWindow().getGuiScaledHeight(); + double d = h / 8; + double p = 2; // quadratic near centre + double k = 0.5 * (d5 * w) / (1 + p * (w / (2 * d) - 1)); + + // linear further out (but continuous at transition point) + double a = k * (1 - p); + double m = p * k / d; + + if (Math.abs(this.accumulatedDX) > d) { + d2 = Math.signum(this.accumulatedDX) * (a + m * Math.abs(this.accumulatedDX)); + } else { + d2 = Math.signum(this.accumulatedDX) * k * Math.pow(Math.abs(this.accumulatedDX) / d, p); + } + + // When going backward, reduce the yaw effect + double h6 = (double) this.minecraft.getWindow().getGuiScaledHeight() / 6; + if (this.accumulatedDY > h6) { + d2 *= 0.5; + } + } + + this.eyemine$resetVelocity(); + + this.minecraft.getTutorial().onMouse(d2, 0); + if (this.minecraft.screen == null && this.minecraft.player != null) { + this.minecraft.player.turn(d2, 0); + } + } else { + this.eyemine$resetVelocity(); + } + } + + /** + * Returns true if the mouse is grabbed. + * We override this so mining works even in ungrabbed mode. + */ + @Inject(method = "isMouseGrabbed()Z", at = @At("RETURN"), cancellable = true) + public void eyemine$isMouseGrabbed(CallbackInfoReturnable cir) { + // Somewhere deep in the MC engine, this is being queried to see whether mining should + // occur, so we have to lie a little. + boolean flag = this.mouseGrabbed; + boolean flag2 = (this.minecraft.isWindowActive() && MouseHelper.ungrabbedMouseMode); + cir.setReturnValue(flag || flag2); + } + + @Inject(method = "grabMouse()V", at = @At(value = "HEAD"), cancellable = true) + public void eyemine$grabMouse(CallbackInfo ci) { + EyeMine.LOGGER.debug("grabMouse"); + if (!MouseHelper.hasGLcontext()) { + ci.cancel(); + } + } + + @Inject(method = "grabMouse()V", at = @At(value = "INVOKE", + target = "Lcom/mojang/blaze3d/platform/InputConstants;grabOrReleaseMouse(JIDD)V", + ordinal = 0), cancellable = true) + public void eyemine$onlyGrabWhenUngrabbed(CallbackInfo ci) { + if (!MouseHelper.ungrabbedMouseMode) { + InputConstants.grabOrReleaseMouse(this.minecraft.getWindow().getWindow(), 212995, this.xpos, this.ypos); + } + + this.minecraft.setScreen((Screen) null); + ((MinecraftAccessor) this.minecraft).setMissTime(10000); + this.ignoreFirstMove = true; + ci.cancel(); + } + + /** + * Resets the player keystate, disables the ingame focus, and ungrabs the mouse cursor. + */ + @Inject(at = @At("HEAD"), method = "releaseMouse()V", cancellable = true) + public void eyemine$releaseMouse(CallbackInfo ci) { + if (!MouseHelper.hasGLcontext()) { + ci.cancel(); + } + } } diff --git a/common/src/main/java/com/specialeffect/eyemine/platform/EyeMineConfig.java b/common/src/main/java/com/specialeffect/eyemine/platform/EyeMineConfig.java index b977555..e8d4e62 100644 --- a/common/src/main/java/com/specialeffect/eyemine/platform/EyeMineConfig.java +++ b/common/src/main/java/com/specialeffect/eyemine/platform/EyeMineConfig.java @@ -188,4 +188,10 @@ public static boolean getSlowdownOnAttack() { // Just throw an error, the content should get replaced at runtime. throw new AssertionError(); } + + @ExpectPlatform + public static int getGazeIdleThreshold() { + // Just throw an error, the content should get replaced at runtime. + throw new AssertionError(); + } } diff --git a/common/src/main/java/com/specialeffect/eyemine/submod/mouse/MouseHandlerMod.java b/common/src/main/java/com/specialeffect/eyemine/submod/mouse/MouseHandlerMod.java index c2198bf..12dc3be 100644 --- a/common/src/main/java/com/specialeffect/eyemine/submod/mouse/MouseHandlerMod.java +++ b/common/src/main/java/com/specialeffect/eyemine/submod/mouse/MouseHandlerMod.java @@ -156,6 +156,8 @@ public void setupInitialState() { if (EyeMineConfig.getUsingMouseEmulation()) { mInputSource = InputSource.Mouse; updateState(InteractionState.MOUSE_NOTHING); + // Mouse emulation uses ungrabbed mode - cursor moves freely + // Position relative to center determines turn rate when in MOUSE_LOOK mode MouseHelper.setUngrabbedMode(true); } else { mInputSource = InputSource.EyeTracker; diff --git a/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze.java b/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze.java index 5058a17..29ccd23 100644 --- a/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze.java +++ b/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze.java @@ -23,6 +23,7 @@ import com.specialeffect.eyemine.submod.misc.ContinuouslyAttack; import com.specialeffect.eyemine.submod.mouse.MouseHandlerMod; import com.specialeffect.eyemine.utils.KeyboardInputHelper; +import com.specialeffect.eyemine.utils.MouseHelper; import com.specialeffect.utils.ModUtils; import dev.architectury.event.EventResult; import dev.architectury.event.events.client.ClientRawInputEvent; @@ -56,11 +57,13 @@ public class MoveWithGaze extends SubMod implements IConfigListener { private static KeyMapping mIncreaseWalkSpeedKB; private static KeyMapping mDecreaseWalkSpeedKB; - private static int mQueueLength = 50; + private static int mQueueLength = 20; private static boolean mMoveWhenMouseStationary = false; public static float mCustomSpeedFactor = 0.8f; + private static boolean mWasPausedByGaze = false; // Track for logging + private int jumpTicks = 0; private final BoatController boatController = new BoatController(0.35, 0.15, 0); @@ -129,6 +132,23 @@ public void onClientTick(Minecraft event) { // hasn't moved at all. This is mainly applicable to gaze input. // - If walking into a wall, don't keep walking fast! + // Pause walking when gaze is below the hotbar (where EyeMine keyboard renders) + // or when gaze is outside the window entirely (original behavior per dev) + // This check must be OUTSIDE the hasPendingEvent() condition because gaze at + // keyboard area may be in the deadzone where no pending event is registered + if (mDoingAutoWalk && null == minecraft.screen && + (MouseHelper.isGazeBelowHotbar || MouseHelper.isGazeOutsideWindow)) { + if (!mWasPausedByGaze) { + LOGGER.info("Walking paused - gaze {} threshold", + MouseHelper.isGazeOutsideWindow ? "outside window" : "below hotbar"); + mWasPausedByGaze = true; + } + KeyboardInputHelper.setWalkOverride(false, 0.0f); + return; + } else { + mWasPausedByGaze = false; + } + if (mDoingAutoWalk && null == minecraft.screen && (mMoveWhenMouseStationary || MouseHandlerMod.hasPendingEvent())) { double forward = (double) mCustomSpeedFactor; @@ -265,7 +285,10 @@ public void onClientTick(Minecraft event) { private double slowdownFactorPitch(Player player) { float f = player.getXRot(); - if (f < -75 || f > 75) { + // Fully stop when looking at extreme angles (e.g., onboard keyboard) + if (f < -80 || f > 80) { + return 0.0f; + } else if (f < -75 || f > 75) { return 0.15f; } else if (f < -60 || f > 60) { return 0.3f; diff --git a/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze2.java b/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze2.java index 3604621..9b7b248 100644 --- a/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze2.java +++ b/common/src/main/java/com/specialeffect/eyemine/submod/movement/MoveWithGaze2.java @@ -92,6 +92,16 @@ public void syncConfig() { public void onClientTick(Minecraft minecraft) { LocalPlayer player = Minecraft.getInstance().player; if (player != null) { + // Pause walking when gaze is below the hotbar (where EyeMine keyboard renders) + // or when gaze is outside the window entirely (original behavior per dev) + // This check must be OUTSIDE the hasPendingEvent() condition because gaze at + // keyboard area may be in the deadzone where no pending event is registered + if (mDoingAutoWalk && minecraft.screen == null && + (MouseHelper.isGazeBelowHotbar || MouseHelper.isGazeOutsideWindow)) { + KeyboardInputHelper.setWalkOverride(false, 0.0f); + return; + } + if (mDoingAutoWalk && minecraft.screen == null && // no gui visible (mMoveWhenMouseStationary || MouseHandlerMod.hasPendingEvent())) { diff --git a/common/src/main/java/com/specialeffect/eyemine/utils/MouseHelper.java b/common/src/main/java/com/specialeffect/eyemine/utils/MouseHelper.java index c8239cf..992e0f3 100644 --- a/common/src/main/java/com/specialeffect/eyemine/utils/MouseHelper.java +++ b/common/src/main/java/com/specialeffect/eyemine/utils/MouseHelper.java @@ -30,11 +30,19 @@ public static MouseHelper instance() { // to access EyeMine keyboard public static boolean ungrabbedMouseMode = false; - private static boolean mHasPendingEvent = false; + public static boolean mHasPendingEvent = false; public static double lastXVelocity = 0.0; public static double lastYVelocity = 0.0; + // Track if gaze is below the hotbar area (where EyeMine keyboard renders) + // Walking should pause when this is true + public static boolean isGazeBelowHotbar = false; + + // Track if gaze is outside the window (in the dead border area) + // Walking should pause when this is true - matches original behavior + public static boolean isGazeOutsideWindow = false; + public static synchronized void addPendingEvent() { mHasPendingEvent = true; } diff --git a/common/src/main/java/com/specialeffect/utils/ModUtils.java b/common/src/main/java/com/specialeffect/utils/ModUtils.java index a04a6d7..9b8ba51 100644 --- a/common/src/main/java/com/specialeffect/utils/ModUtils.java +++ b/common/src/main/java/com/specialeffect/utils/ModUtils.java @@ -249,7 +249,6 @@ private boolean isDirectlyFacingSideHit(Direction sideHit, Vec3 lookVec) { return false; } - @SuppressWarnings("removal") public static BlockPos highestSolidPoint(BlockPos pos) { // Gets a spawn-able location above the point // Highest solid block that isn't foliage @@ -258,8 +257,8 @@ public static BlockPos highestSolidPoint(BlockPos pos) { BlockPos blockpos; BlockPos blockpos1; - //TODO: LevelChunk#getHighestSectionPosition() is deprecated and marked for removal - for (blockpos = new BlockPos(pos.getX(), chunk.getHighestSectionPosition() + 16, pos.getZ()); blockpos.getY() >= 0; blockpos = blockpos1) { + // Start from max build height and search downward + for (blockpos = new BlockPos(pos.getX(), world.getMaxBuildHeight(), pos.getZ()); blockpos.getY() >= world.getMinBuildHeight(); blockpos = blockpos1) { blockpos1 = blockpos.below(); BlockState state = chunk.getBlockState(blockpos1); diff --git a/fabric/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java b/fabric/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java index b9feee8..1e97238 100644 --- a/fabric/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java +++ b/fabric/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java @@ -88,14 +88,19 @@ public static class Movement { public boolean slowdownOnCorners = true; @Comment("How many ticks to take into account for slowing down while looking around / turning corners. " + - " (smaller number = faster)") + " (smaller number = faster, 20 = 1 second)") @BoundedDiscrete(min = 1, max = 200) - public int walkingSlowdownFilter = 10; + public int walkingSlowdownFilter = 20; @Comment("Continue walking forward when the mouse is stationary?" + " Recommended to be turned off for eye gaze control, or turned on for joysticks.") public boolean moveWhenMouseStationary = false; + @Comment("Percentage of screen height from bottom where gaze pauses walking" + + " (for looking at onboard keyboard). 0 = disabled, 10 = bottom 10% of screen") + @BoundedDiscrete(min = 0, max = 50) + public int gazeIdleThreshold = 10; + @Comment("Slow down auto-walk when attacking an entity" + " This only applies when your crosshair is over an entity, and makes it easier to chase mobs") public boolean slowdownOnAttack = true; diff --git a/fabric/src/main/java/com/specialeffect/eyemine/platform/fabric/EyeMineConfigImpl.java b/fabric/src/main/java/com/specialeffect/eyemine/platform/fabric/EyeMineConfigImpl.java index 6c9e52f..297b238 100644 --- a/fabric/src/main/java/com/specialeffect/eyemine/platform/fabric/EyeMineConfigImpl.java +++ b/fabric/src/main/java/com/specialeffect/eyemine/platform/fabric/EyeMineConfigImpl.java @@ -158,4 +158,9 @@ public static boolean getSlowdownOnAttack() { EyeMineConfig config = AutoConfig.getConfigHolder(EyeMineConfig.class).getConfig(); return config.movement.slowdownOnAttack; } + + public static int getGazeIdleThreshold() { + EyeMineConfig config = AutoConfig.getConfigHolder(EyeMineConfig.class).getConfig(); + return config.movement.gazeIdleThreshold; + } } diff --git a/forge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java b/forge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java index 8c40bab..770a5ee 100644 --- a/forge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java +++ b/forge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java @@ -48,6 +48,7 @@ public class EyeMineConfig { public static ForgeConfigSpec.IntValue walkingSlowdownFilter; public static ForgeConfigSpec.BooleanValue moveWhenMouseStationary; public static ForgeConfigSpec.DoubleValue customSpeedFactor; + public static ForgeConfigSpec.IntValue gazeIdleThreshold; public static ForgeConfigSpec.BooleanValue slowdownOnCorners; public static ForgeConfigSpec.BooleanValue slowdownOnAttack; @@ -197,13 +198,17 @@ private static void setupMovingConfig() { .define("slowdownOnCorners", true); walkingSlowdownFilter = CLIENT_BUILDER.comment( - "How many ticks to take into account for slowing down while looking around / turning corners.\n(smaller number = faster)") - .defineInRange("walkingSlowdownFilter", 10, 1, 200); + "How many ticks to take into account for slowing down while looking around / turning corners.\n(smaller number = faster, 20 = 1 second)") + .defineInRange("walkingSlowdownFilter", 20, 1, 200); moveWhenMouseStationary = CLIENT_BUILDER.comment( "Continue walking forward when the mouse is stationary?\nRecommended to be turned off for eye gaze control, or turned on for joysticks.") .define("moveWhenMouseStationary", false); + gazeIdleThreshold = CLIENT_BUILDER.comment( + "Percentage of screen height from bottom where gaze pauses walking\n(for looking at onboard keyboard). 0 = disabled, 10 = bottom 10% of screen") + .defineInRange("gazeIdleThreshold", 10, 0, 50); + slowdownOnAttack = CLIENT_BUILDER.comment( "Slow down auto-walk when attacking an entity\nThis only applies when your crosshair is over an entity, and makes\nit easier to chase mobs") .define("slowdownOnAttack", true); diff --git a/forge/src/main/java/com/specialeffect/eyemine/platform/forge/EyeMineConfigImpl.java b/forge/src/main/java/com/specialeffect/eyemine/platform/forge/EyeMineConfigImpl.java index a3ac81a..eeb6f1a 100644 --- a/forge/src/main/java/com/specialeffect/eyemine/platform/forge/EyeMineConfigImpl.java +++ b/forge/src/main/java/com/specialeffect/eyemine/platform/forge/EyeMineConfigImpl.java @@ -126,4 +126,8 @@ public static boolean getDwellShowExpanding() { public static boolean getSlowdownOnAttack() { return EyeMineConfig.slowdownOnAttack.get(); } + + public static int getGazeIdleThreshold() { + return EyeMineConfig.gazeIdleThreshold.get(); + } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..94113f2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 76fc418..fa83b3f 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -40,7 +40,7 @@ dependencies { processResources { inputs.property "version", project.version - filesMatching("META-INF/mods.toml") { + filesMatching("META-INF/neoforge.mods.toml") { expand "version": project.version } } diff --git a/neoforge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java b/neoforge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java index 7cd829e..e5d2190 100644 --- a/neoforge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java +++ b/neoforge/src/main/java/com/specialeffect/eyemine/config/EyeMineConfig.java @@ -43,6 +43,7 @@ public class EyeMineConfig { public static ModConfigSpec.IntValue walkingSlowdownFilter; public static ModConfigSpec.BooleanValue moveWhenMouseStationary; public static ModConfigSpec.DoubleValue customSpeedFactor; + public static ModConfigSpec.IntValue gazeIdleThreshold; public static ModConfigSpec.BooleanValue slowdownOnCorners; public static ModConfigSpec.BooleanValue slowdownOnAttack; @@ -192,13 +193,17 @@ private static void setupMovingConfig() { .define("slowdownOnCorners", true); walkingSlowdownFilter = CLIENT_BUILDER.comment( - "How many ticks to take into account for slowing down while looking around / turning corners.\n(smaller number = faster)") - .defineInRange("walkingSlowdownFilter", 10, 1, 200); + "How many ticks to take into account for slowing down while looking around / turning corners.\n(smaller number = faster, 20 = 1 second)") + .defineInRange("walkingSlowdownFilter", 20, 1, 200); moveWhenMouseStationary = CLIENT_BUILDER.comment( "Continue walking forward when the mouse is stationary?\nRecommended to be turned off for eye gaze control, or turned on for joysticks.") .define("moveWhenMouseStationary", false); + gazeIdleThreshold = CLIENT_BUILDER.comment( + "Percentage of screen height from bottom where gaze pauses walking\n(for looking at onboard keyboard). 0 = disabled, 10 = bottom 10% of screen") + .defineInRange("gazeIdleThreshold", 10, 0, 50); + slowdownOnAttack = CLIENT_BUILDER.comment( "Slow down auto-walk when attacking an entity\nThis only applies when your crosshair is over an entity, and makes\nit easier to chase mobs") .define("slowdownOnAttack", true); diff --git a/neoforge/src/main/java/com/specialeffect/eyemine/neoforge/EyeMineNeoForge.java b/neoforge/src/main/java/com/specialeffect/eyemine/neoforge/EyeMineNeoForge.java index 949f110..6d9473f 100644 --- a/neoforge/src/main/java/com/specialeffect/eyemine/neoforge/EyeMineNeoForge.java +++ b/neoforge/src/main/java/com/specialeffect/eyemine/neoforge/EyeMineNeoForge.java @@ -15,6 +15,8 @@ @Mod(EyeMine.MOD_ID) public class EyeMineNeoForge { public EyeMineNeoForge(IEventBus eventBus, ModContainer container, Dist dist) { + // In NeoForge 1.21+, the event bus is passed directly to the constructor + // and Architectury automatically detects it via ModList.getModContainerById() EyeMine.init(); if (dist.isClient()) { diff --git a/neoforge/src/main/java/com/specialeffect/eyemine/platform/neoforge/EyeMineConfigImpl.java b/neoforge/src/main/java/com/specialeffect/eyemine/platform/neoforge/EyeMineConfigImpl.java index 645e1cf..4bb0521 100644 --- a/neoforge/src/main/java/com/specialeffect/eyemine/platform/neoforge/EyeMineConfigImpl.java +++ b/neoforge/src/main/java/com/specialeffect/eyemine/platform/neoforge/EyeMineConfigImpl.java @@ -126,4 +126,8 @@ public static boolean getDwellShowExpanding() { public static boolean getSlowdownOnAttack() { return EyeMineConfig.slowdownOnAttack.get(); } + + public static int getGazeIdleThreshold() { + return EyeMineConfig.gazeIdleThreshold.get(); + } } diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 672f865..3cd4b4d 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -5,7 +5,7 @@ license="GPL-3.0" [[mods]] modId="eyemine" -version="${file.jarVersion}" +version="${version}" displayName="EyeMine" displayURL="https://www.specialeffect.org.uk/eyemine" authors="Kirsty McNaught, Mrbysco"