refactor(aim): split RotationManager to 199 lines

State machine, mutable state and server-rotation packet tracking move to
rotation/internal/; the singleton, event wiring and public API stay put.
This commit is contained in:
loki5512344 2026-09-30 11:17:03 +02:00
parent 7e8b6b6467
commit 3e57542bad
Signed by: boba
GPG key ID: 253067914055423B
4 changed files with 329 additions and 228 deletions

View file

@ -1,57 +1,39 @@
package dev.loki.lovisual.util.aiming.rotation; package dev.loki.lovisual.util.aiming.rotation;
import dev.loki.lovisual.util.aiming.RequestHandler;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.network.protocol.game.ServerboundUseItemPacket;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.events.meta.EventHandler;
import dev.loki.lovisual.events.Events; import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.input.RotationUpdateEvent;
import dev.loki.lovisual.events.impl.network.EventSync; import dev.loki.lovisual.events.impl.network.EventSync;
import dev.loki.lovisual.events.impl.network.PacketEvent; import dev.loki.lovisual.events.impl.network.PacketEvent;
import dev.loki.lovisual.events.impl.world.PlayerVelocityStrafe; import dev.loki.lovisual.events.impl.world.PlayerVelocityStrafe;
import dev.loki.lovisual.events.impl.input.RotationUpdateEvent; import dev.loki.lovisual.events.meta.EventHandler;
import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; import dev.loki.lovisual.mixininterface.entity.ILocalPlayer;
import dev.loki.lovisual.mixins.accessors.entity.EntityInvoker; import dev.loki.lovisual.mixins.accessors.entity.EntityInvoker;
import dev.loki.lovisual.util.aiming.data.Rotation; import dev.loki.lovisual.util.aiming.data.Rotation;
import dev.loki.lovisual.util.aiming.features.MovementCorrection; import dev.loki.lovisual.util.aiming.features.MovementCorrection;
import dev.loki.lovisual.util.aiming.rotation.internal.RotationPackets;
import dev.loki.lovisual.util.aiming.rotation.internal.RotationState;
import dev.loki.lovisual.util.aiming.rotation.internal.RotationStepper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
/**
* Public rotation facade: singleton entry point, event wiring and accessors.
* The state machine lives in {@link RotationStepper}, state in
* {@link RotationState} and server-rotation tracking in {@link RotationPackets}.
*/
public final class RotationManager { public final class RotationManager {
public static final RotationManager INSTANCE = new RotationManager(); public static final RotationManager INSTANCE = new RotationManager();
private final RequestHandler<RotationTarget> rotationTargetHandler = new RequestHandler<>(); private final RotationState state = new RotationState();
private RotationTarget previousRotationTarget; private final RotationPackets packets = new RotationPackets();
private Object previousRotationProvider;
private Rotation currentRotation;
private Rotation previousRotation;
private Object currentRotationProvider;
private boolean smoothReturnActive;
private int smoothReturnTicks;
private Rotation actualServerRotation = Rotation.ZERO;
private Rotation theoreticalServerRotation = Rotation.ZERO;
private int lastLifecycleAge = Integer.MIN_VALUE; private int lastLifecycleAge = Integer.MIN_VALUE;
private RotationManager() { private RotationManager() {
} }
private static float computeRotationDifference(Rotation a, Rotation b) {
if (a == null || b == null) {
return Float.MAX_VALUE;
}
float yawDiff = Math.abs(RotationUtil.angleDifference(a.yaw(), b.yaw()));
float pitchDiff = Math.abs(a.pitch() - b.pitch());
return (float) Math.hypot(yawDiff, pitchDiff);
}
public static LocalPlayer player() { public static LocalPlayer player() {
return Minecraft.getInstance().player; return Minecraft.getInstance().player;
} }
@ -85,78 +67,48 @@ public final class RotationManager {
} }
public RotationTarget getActiveRotationTarget() { public RotationTarget getActiveRotationTarget() {
RotationTarget active = rotationTargetHandler.getActiveRequestValue(); return state.activeTarget();
return active != null ? active : previousRotationTarget;
} }
public RotationTarget getPreviousRotationTarget() { public RotationTarget getPreviousRotationTarget() {
return previousRotationTarget; return state.previousTarget();
} }
public Rotation getCurrentRotation() { public Rotation getCurrentRotation() {
return currentRotation; return state.current();
}
private void setCurrentRotation(Rotation rotation) {
setCurrentRotation(rotation, currentRotationProvider);
}
private void setCurrentRotation(Rotation rotation, Object provider) {
if (rotation == null) {
if (currentRotation != null) {
previousRotation = currentRotation;
}
currentRotation = null;
currentRotationProvider = null;
return;
}
previousRotation = currentRotation;
currentRotation = rotation;
currentRotationProvider = provider;
} }
public Rotation getPreviousRotation() { public Rotation getPreviousRotation() {
return previousRotation; return state.previous();
} }
public Rotation getServerRotation() { public Rotation getServerRotation() {
return actualServerRotation; return packets.actual();
} }
public Rotation getMovementRotation() { public Rotation getMovementRotation() {
LocalPlayer player = player(); LocalPlayer player = player();
Rotation current = state.current();
Rotation fallback = player != null Rotation fallback = player != null
? new Rotation(player.getYRot(), player.getXRot(), true) ? new Rotation(player.getYRot(), player.getXRot(), true)
: (currentRotation != null ? currentRotation : Rotation.ZERO); : (current != null ? current : Rotation.ZERO);
RotationTarget active = getActiveRotationTarget(); RotationTarget active = getActiveRotationTarget();
if (currentRotation == null || active == null || active.movementCorrection == MovementCorrection.OFF) { if (current == null || active == null || active.movementCorrection == MovementCorrection.OFF) {
return fallback; return fallback;
} }
return currentRotation; return current;
} }
public void setRotationTarget(RotationTarget plan, int priority, Object provider) { public void setRotationTarget(RotationTarget plan, int priority, Object provider) {
if (plan == null) return; state.request(plan, priority, provider);
rotationTargetHandler.request(new RequestHandler.Request<>(plan.ticksUntilReset, priority, provider, plan));
smoothReturnActive = false;
smoothReturnTicks = 0;
} }
public void setRotationTarget(RotationTarget plan, int priority) { public void setRotationTarget(RotationTarget plan, int priority) {
setRotationTarget(plan, priority, null); setRotationTarget(plan, priority, null);
} }
private boolean isRotatingAllowed(RotationTarget target) {
if (!target.considerInventory) return true;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return true;
return !(ClientScreen.current() instanceof AbstractContainerScreen<?>);
}
/** /**
* Update current rotation to a new rotation step. * Update current rotation to a new rotation step.
*/ */
@ -167,124 +119,15 @@ public final class RotationManager {
return; return;
} }
Rotation playerRotation = new Rotation(player.getYRot(), player.getXRot(), true); RotationStepper.step(state, player, packets.actual());
RequestHandler.Request<RotationTarget> activeRequest = rotationTargetHandler.getActiveRequest();
RotationTarget activeRotationTarget = activeRequest != null ? activeRequest.value() : previousRotationTarget;
Object activeProvider = activeRequest != null ? activeRequest.provider() : previousRotationProvider;
if (activeRotationTarget == null) {
if (currentRotation == null || !smoothReturnActive) {
return;
}
float diffToPlayer = computeRotationDifference(currentRotation, playerRotation);
if (diffToPlayer <= 0.5f) {
finishSmoothReturn();
return;
}
float speed = 0.25f + 0.4f * Math.min(1.0f, diffToPlayer / 30.0f);
float yawDiff = RotationUtil.angleDifference(playerRotation.yaw(), currentRotation.yaw());
float newYaw = currentRotation.yaw() + yawDiff * speed;
float newPitch = Mth.lerp(speed, currentRotation.pitch(), playerRotation.pitch());
setCurrentRotation(new Rotation(newYaw, newPitch, false).normalize(), currentRotationProvider);
return;
}
if (isRotatingAllowed(activeRotationTarget)) {
Rotation fromRotation = resolveBaseRotation(playerRotation);
if (activeRequest == null) {
if (currentRotation == null) {
finishSmoothReturn();
return;
}
if (!smoothReturnActive) {
smoothReturnActive = true;
smoothReturnTicks = 0;
}
Rotation resetRotation = activeRotationTarget.towards(fromRotation, true).normalize();
setCurrentRotation(resetRotation, activeProvider);
previousRotationTarget = activeRotationTarget;
previousRotationProvider = activeProvider;
smoothReturnTicks++;
float diffToPlayer = computeRotationDifference(resetRotation, playerRotation);
int maxTicks = Math.max(1, activeRotationTarget.ticksUntilReset);
float threshold = Math.max(0.05f, activeRotationTarget.resetThreshold);
if (diffToPlayer <= threshold || smoothReturnTicks >= maxTicks) {
finishSmoothReturn();
rotationTargetHandler.tick();
return;
}
} else {
Rotation rotation = activeRotationTarget.towards(fromRotation, false).normalize();
setCurrentRotation(rotation, activeProvider);
previousRotationTarget = activeRotationTarget;
previousRotationProvider = activeProvider;
smoothReturnActive = false;
smoothReturnTicks = 0;
if (activeRotationTarget.whenReached != null) {
activeRotationTarget.whenReached.invoke();
}
}
}
rotationTargetHandler.tick();
}
private void finishSmoothReturn() {
setCurrentRotation(null);
previousRotation = null;
previousRotationTarget = null;
previousRotationProvider = null;
smoothReturnActive = false;
smoothReturnTicks = 0;
}
private Rotation resolveBaseRotation(Rotation playerRotation) {
if (currentRotation != null) {
return currentRotation;
}
if (actualServerRotation != null && actualServerRotation != Rotation.ZERO) {
return actualServerRotation;
}
return playerRotation;
} }
public void clear() { public void clear() {
rotationTargetHandler.clear(); state.clearAll();
previousRotationTarget = null;
previousRotationProvider = null;
currentRotation = null;
currentRotationProvider = null;
previousRotation = null;
smoothReturnActive = false;
smoothReturnTicks = 0;
} }
public void clear(Object provider) { public void clear(Object provider) {
if (provider == null) return; state.clearProvider(provider);
boolean removed = rotationTargetHandler.clear(provider);
boolean ownsVisualState = provider == currentRotationProvider || provider == previousRotationProvider;
if (!removed && !ownsVisualState) {
return;
}
if (currentRotation != null && ownsVisualState) {
smoothReturnActive = true;
smoothReturnTicks = 0;
return;
}
if (ownsVisualState) {
previousRotationTarget = null;
previousRotationProvider = null;
previousRotation = null;
}
smoothReturnActive = false;
smoothReturnTicks = 0;
} }
/** /**
@ -296,33 +139,16 @@ public final class RotationManager {
} }
public void release(Object provider, boolean smoothReturn) { public void release(Object provider, boolean smoothReturn) {
if (provider == null) return; state.releaseProvider(provider, smoothReturn);
boolean removed = rotationTargetHandler.clear(provider);
boolean ownsVisualState = provider == currentRotationProvider || provider == previousRotationProvider;
if (!removed && !ownsVisualState) {
return;
}
if (!smoothReturn) {
if (ownsVisualState) {
setCurrentRotation(null);
previousRotationTarget = null;
previousRotationProvider = null;
previousRotation = null;
}
smoothReturnActive = false;
smoothReturnTicks = 0;
return;
}
smoothReturnActive = ownsVisualState && currentRotation != null;
smoothReturnTicks = 0;
} }
@EventHandler @EventHandler
public void onSync(EventSync e) { public void onSync(EventSync e) {
RotationTarget activeRotationTarget = getActiveRotationTarget(); RotationTarget active = state.activeTarget();
if (activeRotationTarget == null || currentRotation == null) return; Rotation current = state.current();
if (active == null || current == null) return;
e.setRotation(currentRotation.yaw(), currentRotation.pitch(), true); e.setRotation(current.yaw(), current.pitch(), true);
} }
@EventHandler(priority = 100) @EventHandler(priority = 100)
@ -346,30 +172,12 @@ public final class RotationManager {
@EventHandler(priority = -100) @EventHandler(priority = -100)
public void onPacketSend(PacketEvent.Send e) { public void onPacketSend(PacketEvent.Send e) {
if (e.isCancelled()) return; packets.onSend(e);
var packet = e.getPacket();
Rotation rot = null;
if (packet instanceof ServerboundMovePlayerPacket move) {
if (!move.hasRotation()) return;
rot = new Rotation(move.getYRot(0.0f), move.getXRot(0.0f), true);
} else if (packet instanceof ServerboundUseItemPacket use) {
rot = new Rotation(use.getYRot(), use.getXRot(), true);
}
if (rot != null) {
actualServerRotation = rot;
theoreticalServerRotation = rot;
}
} }
@EventHandler(priority = -100) @EventHandler(priority = -100)
public void onPacketReceive(PacketEvent.Receive e) { public void onPacketReceive(PacketEvent.Receive e) {
if (!(e.getPacket() instanceof net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket packet)) packets.onReceive(e);
return;
Rotation rot = new Rotation(packet.change().yRot(), packet.change().xRot(), true);
theoreticalServerRotation = rot;
actualServerRotation = rot;
} }
public void runTickLifecycle() { public void runTickLifecycle() {

View file

@ -0,0 +1,50 @@
package dev.loki.lovisual.util.aiming.rotation.internal;
import dev.loki.lovisual.events.impl.network.PacketEvent;
import dev.loki.lovisual.util.aiming.data.Rotation;
import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.network.protocol.game.ServerboundUseItemPacket;
/**
* Tracks the rotation the server sees, updated from outbound and inbound
* movement packets. Kept apart from the facade so packet wiring stays small.
*/
public final class RotationPackets {
private Rotation actualServerRotation = Rotation.ZERO;
private Rotation theoreticalServerRotation = Rotation.ZERO;
public Rotation actual() {
return actualServerRotation;
}
public void onSend(PacketEvent.Send e) {
if (e.isCancelled()) return;
var packet = e.getPacket();
Rotation rot = null;
if (packet instanceof ServerboundMovePlayerPacket move) {
if (!move.hasRotation()) return;
rot = new Rotation(move.getYRot(0.0f), move.getXRot(0.0f), true);
} else if (packet instanceof ServerboundUseItemPacket use) {
rot = new Rotation(use.getYRot(), use.getXRot(), true);
}
if (rot != null) {
actualServerRotation = rot;
theoreticalServerRotation = rot;
}
}
public void onReceive(PacketEvent.Receive e) {
if (!(e.getPacket() instanceof ClientboundPlayerPositionPacket packet)) return;
Rotation rot = new Rotation(packet.change().yRot(), packet.change().xRot(), true);
theoreticalServerRotation = rot;
actualServerRotation = rot;
}
public Rotation theoretical() {
return theoreticalServerRotation;
}
}

View file

@ -0,0 +1,128 @@
package dev.loki.lovisual.util.aiming.rotation.internal;
import dev.loki.lovisual.util.aiming.RequestHandler;
import dev.loki.lovisual.util.aiming.data.Rotation;
import dev.loki.lovisual.util.aiming.rotation.RotationTarget;
/**
* Mutable rotation state shared by {@link RotationStepper} and the public
* {@code RotationManager} facade. Fields are package-private on purpose so the
* stepper can drive them in place, mirroring the original single-class logic.
*/
public final class RotationState {
final RequestHandler<RotationTarget> handler = new RequestHandler<>();
RotationTarget previousTarget;
Object previousProvider;
Rotation current;
Rotation previous;
Object currentProvider;
boolean smoothReturnActive;
int smoothReturnTicks;
public RotationTarget activeTarget() {
RotationTarget active = handler.getActiveRequestValue();
return active != null ? active : previousTarget;
}
public RotationTarget previousTarget() {
return previousTarget;
}
public Rotation current() {
return current;
}
public Rotation previous() {
return previous;
}
public void request(RotationTarget plan, int priority, Object provider) {
if (plan == null) return;
handler.request(new RequestHandler.Request<>(plan.ticksUntilReset, priority, provider, plan));
smoothReturnActive = false;
smoothReturnTicks = 0;
}
void setCurrent(Rotation rotation, Object provider) {
if (rotation == null) {
if (current != null) {
previous = current;
}
current = null;
currentProvider = null;
return;
}
previous = current;
current = rotation;
currentProvider = provider;
}
void finishSmoothReturn() {
setCurrent(null, currentProvider);
previous = null;
previousTarget = null;
previousProvider = null;
smoothReturnActive = false;
smoothReturnTicks = 0;
}
public void clearAll() {
handler.clear();
previousTarget = null;
previousProvider = null;
current = null;
currentProvider = null;
previous = null;
smoothReturnActive = false;
smoothReturnTicks = 0;
}
public void clearProvider(Object provider) {
if (provider == null) return;
boolean removed = handler.clear(provider);
boolean ownsVisualState = provider == currentProvider || provider == previousProvider;
if (!removed && !ownsVisualState) {
return;
}
if (current != null && ownsVisualState) {
smoothReturnActive = true;
smoothReturnTicks = 0;
return;
}
if (ownsVisualState) {
previousTarget = null;
previousProvider = null;
previous = null;
}
smoothReturnActive = false;
smoothReturnTicks = 0;
}
public void releaseProvider(Object provider, boolean smoothReturn) {
if (provider == null) return;
boolean removed = handler.clear(provider);
boolean ownsVisualState = provider == currentProvider || provider == previousProvider;
if (!removed && !ownsVisualState) {
return;
}
if (!smoothReturn) {
if (ownsVisualState) {
setCurrent(null, currentProvider);
previousTarget = null;
previousProvider = null;
previous = null;
}
smoothReturnActive = false;
smoothReturnTicks = 0;
return;
}
smoothReturnActive = ownsVisualState && current != null;
smoothReturnTicks = 0;
}
}

View file

@ -0,0 +1,115 @@
package dev.loki.lovisual.util.aiming.rotation.internal;
import dev.loki.lovisual.util.aiming.RequestHandler;
import dev.loki.lovisual.util.aiming.data.Rotation;
import dev.loki.lovisual.util.aiming.rotation.RotationTarget;
import dev.loki.lovisual.util.aiming.rotation.RotationUtil;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;
/**
* Per-tick rotation state machine: resolves the active target, advances the
* current rotation and drives the smooth return back to the player view.
*/
public final class RotationStepper {
private RotationStepper() {
}
public static void step(RotationState state, LocalPlayer player, Rotation serverRotation) {
Rotation playerRotation = new Rotation(player.getYRot(), player.getXRot(), true);
RequestHandler.Request<RotationTarget> activeRequest = state.handler.getActiveRequest();
RotationTarget activeRotationTarget = activeRequest != null ? activeRequest.value() : state.previousTarget;
Object activeProvider = activeRequest != null ? activeRequest.provider() : state.previousProvider;
if (activeRotationTarget == null) {
if (state.current == null || !state.smoothReturnActive) {
return;
}
float diffToPlayer = computeRotationDifference(state.current, playerRotation);
if (diffToPlayer <= 0.5f) {
state.finishSmoothReturn();
return;
}
float speed = 0.25f + 0.4f * Math.min(1.0f, diffToPlayer / 30.0f);
float yawDiff = RotationUtil.angleDifference(playerRotation.yaw(), state.current.yaw());
float newYaw = state.current.yaw() + yawDiff * speed;
float newPitch = Mth.lerp(speed, state.current.pitch(), playerRotation.pitch());
state.setCurrent(new Rotation(newYaw, newPitch, false).normalize(), state.currentProvider);
return;
}
if (isRotatingAllowed(activeRotationTarget)) {
Rotation fromRotation = resolveBaseRotation(state, playerRotation, serverRotation);
if (activeRequest == null) {
if (state.current == null) {
state.finishSmoothReturn();
return;
}
if (!state.smoothReturnActive) {
state.smoothReturnActive = true;
state.smoothReturnTicks = 0;
}
Rotation resetRotation = activeRotationTarget.towards(fromRotation, true).normalize();
state.setCurrent(resetRotation, activeProvider);
state.previousTarget = activeRotationTarget;
state.previousProvider = activeProvider;
state.smoothReturnTicks++;
float diffToPlayer = computeRotationDifference(resetRotation, playerRotation);
int maxTicks = Math.max(1, activeRotationTarget.ticksUntilReset);
float threshold = Math.max(0.05f, activeRotationTarget.resetThreshold);
if (diffToPlayer <= threshold || state.smoothReturnTicks >= maxTicks) {
state.finishSmoothReturn();
state.handler.tick();
return;
}
} else {
Rotation rotation = activeRotationTarget.towards(fromRotation, false).normalize();
state.setCurrent(rotation, activeProvider);
state.previousTarget = activeRotationTarget;
state.previousProvider = activeProvider;
state.smoothReturnActive = false;
state.smoothReturnTicks = 0;
if (activeRotationTarget.whenReached != null) {
activeRotationTarget.whenReached.invoke();
}
}
}
state.handler.tick();
}
private static float computeRotationDifference(Rotation a, Rotation b) {
if (a == null || b == null) {
return Float.MAX_VALUE;
}
float yawDiff = Math.abs(RotationUtil.angleDifference(a.yaw(), b.yaw()));
float pitchDiff = Math.abs(a.pitch() - b.pitch());
return (float) Math.hypot(yawDiff, pitchDiff);
}
private static boolean isRotatingAllowed(RotationTarget target) {
if (!target.considerInventory) return true;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return true;
return !(ClientScreen.current() instanceof AbstractContainerScreen<?>);
}
private static Rotation resolveBaseRotation(RotationState state, Rotation playerRotation, Rotation serverRotation) {
if (state.current != null) {
return state.current;
}
if (serverRotation != null && serverRotation != Rotation.ZERO) {
return serverRotation;
}
return playerRotation;
}
}