Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public class InGameInputHandler {
private boolean gyroToggledOn;
private boolean wasAiming;
private Animation flickAnimation;
private final MouseCountQuantizer mouseLook = new MouseCountQuantizer();
private LocalPlayer lookPlayer;

private boolean shouldShowPlayerList;

Expand Down Expand Up @@ -335,10 +337,13 @@ protected void handlePlayerLookInput(boolean isController) {
LocalPlayer player = this.minecraft.player;

if (!isController || !canProcessLookInput()) {
lookInputX = 0;
lookInputY = 0;
resetLookInput();
return;
}
if (lookPlayer != player) {
resetLookInput();
lookPlayer = player;
}

boolean aiming = isAiming(player);

Expand Down Expand Up @@ -469,17 +474,37 @@ protected void handleFlickStick(LocalPlayer player) {

flickAnimation = Animation.of(8)
.easing(EasingFunction.EASE_OUT_EXPO)
.deltaConsumerD(angle -> player.turn(angle, 0), 0, flickAngle / 0.15)
.deltaConsumerD(angle -> turnPlayer(player, angle, 0), 0, flickAngle / 0.15)
.play();
}

public void processPlayerLook(float deltaTime) {
if (minecraft.player != null) {
double velX = lookInputX / 0.15 * deltaTime;
double velY = lookInputY / 0.15 * deltaTime;
//noinspection SuspiciousNameCombination
minecraft.player.turn(velX, velY);
minecraft.getTutorial().onMouse(velX, velY);
turnPlayer(minecraft.player, lookInputX / 0.15 * deltaTime, lookInputY / 0.15 * deltaTime);
}

private void turnPlayer(LocalPlayer player, double x, double y) {
// Input is sampled on ticks; focus and screens can change before the next render frame.
if (player == null || player != minecraft.player || player != lookPlayer
|| controlify.inGameInputHandler().orElse(null) != this
|| !ControllerPlayerMovement.shouldBeControllerInput() || !canProcessLookInput()) {
resetLookInput();
return;
}
boolean scoping = !minecraft.options.smoothCamera
&& minecraft.options.getCameraType().isFirstPerson() && player.isScoping();
double scale = MouseCountQuantizer.mouseScale(minecraft.options.sensitivity().get(), scoping);
var turn = mouseLook.quantize(x, y, scale, player.getXRot());
player.turn(turn.x(), turn.y());
minecraft.getTutorial().onMouse(turn.x(), turn.y());
}

private void resetLookInput() {
lookInputX = lookInputY = 0;
lookPlayer = null;
mouseLook.reset();
if (flickAnimation != null) {
flickAnimation.abort();
flickAnimation = null;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2026 isXander
* This file is part of Controlify.
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
package dev.isxander.controlify.ingame;

/** Whole mouse counts, with fractional input carried between render frames. */
final class MouseCountQuantizer {

private double remainderX, remainderY;
private double previousScale = Double.NaN;

record Turn(double x, double y) { }

static double mouseScale(double sensitivity, boolean scoping) {
// Match MouseHandler, including its float constants promoted to double.
double base = sensitivity * 0.6F + 0.2F;
return base * base * base * (scoping ? 1 : 8);
}

Turn quantize(double x, double y, double scale, float pitch) {
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(scale)
|| scale <= 0 || !Float.isFinite(pitch)) {
reset();
return new Turn(0, 0);
}
if (scale != previousScale) {
reset();
previousScale = scale;
}
if (pitch >= 90 && y > 0 || pitch <= -90 && y < 0) {
remainderY = 0;
y = 0;
}

double countsX = x / scale + remainderX;
double countsY = y / scale + remainderY;
if (!Double.isFinite(countsX) || !Double.isFinite(countsY)) {
reset();
return new Turn(0, 0);
}
double wholeX = x == 0 ? 0 : Math.rint(countsX);
double wholeY = y == 0 ? 0 : Math.rint(countsY);
remainderX = countsX - wholeX;
remainderY = countsY - wholeY;
Turn turn = new Turn(wholeX * scale, wholeY * scale);

float nextPitch = pitch + (float) turn.y() * 0.15F;
// Discard input clipped by vanilla, but keep tiny inward input at the limit.
if (turn.y() > 0 && nextPitch >= 90 || turn.y() < 0 && nextPitch <= -90) remainderY = 0;
return turn;
}

void reset() {
remainderX = remainderY = 0;
previousScale = Double.NaN;
}
}