diff --git a/src/api/java/baritone/api/behavior/ILookBehavior.java b/src/api/java/baritone/api/behavior/ILookBehavior.java
index 218e8b59a..eb7c992b8 100644
--- a/src/api/java/baritone/api/behavior/ILookBehavior.java
+++ b/src/api/java/baritone/api/behavior/ILookBehavior.java
@@ -17,6 +17,8 @@
package baritone.api.behavior;
+import baritone.api.Settings;
+import baritone.api.behavior.look.IAimProcessor;
import baritone.api.utils.Rotation;
/**
@@ -27,10 +29,25 @@ public interface ILookBehavior extends IBehavior {
/**
* Updates the current {@link ILookBehavior} target to target the specified rotations on the next tick. If any sort
- * of block interaction is required, {@code blockInteract} should be {@code true}.
+ * of block interaction is required, {@code blockInteract} should be {@code true}. It is not guaranteed that the
+ * rotations set by the caller will be the exact rotations expressed by the client (This is due to settings like
+ * {@link Settings#randomLooking}). If the rotations produced by this behavior are required, then the
+ * {@link #getAimProcessor() aim processor} should be used.
*
* @param rotation The target rotations
* @param blockInteract Whether the target rotations are needed for a block interaction
*/
void updateTarget(Rotation rotation, boolean blockInteract);
+
+ /**
+ * The aim processor instance for this {@link ILookBehavior}, which is responsible for applying additional, deterministic
+ * transformations to the target rotation set by {@link #updateTarget}. Whenever {@link IAimProcessor#nextRotation(Rotation)}
+ * is called on the instance returned by this method, the returned value always reflects what would happen in the
+ * upcoming tick. In other words, it is a pure function, and no internal state changes. If simulation of the
+ * rotation states beyond the next tick is required, then a {@link IAimProcessor#fork(int) fork} should be created.
+ *
+ * @return The aim processor
+ * @see IAimProcessor#fork(int)
+ */
+ IAimProcessor getAimProcessor();
}
diff --git a/src/api/java/baritone/api/behavior/look/IAimProcessor.java b/src/api/java/baritone/api/behavior/look/IAimProcessor.java
new file mode 100644
index 000000000..72a4c681d
--- /dev/null
+++ b/src/api/java/baritone/api/behavior/look/IAimProcessor.java
@@ -0,0 +1,44 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone. If not, see .
+ */
+
+package baritone.api.behavior.look;
+
+import baritone.api.utils.Rotation;
+
+/**
+ * @author Brady
+ */
+public interface IAimProcessor {
+
+ /**
+ * Returns the actual rotation that will be used when the desired rotation is requested. This is not guaranteed to
+ * return the same value for a given input.
+ *
+ * @param desired The desired rotation to set
+ * @return The actual rotation
+ */
+ Rotation nextRotation(Rotation desired);
+
+ /**
+ * Returns a copy of this {@link IAimProcessor} which has its own internal state and updates on each call to
+ * {@link #nextRotation(Rotation)}.
+ *
+ * @param ticksAdvanced The number of ticks to advance ahead of time
+ * @return The forked processor
+ */
+ IAimProcessor fork(int ticksAdvanced);
+}
diff --git a/src/main/java/baritone/behavior/LookBehavior.java b/src/main/java/baritone/behavior/LookBehavior.java
index d3c514ac2..aeb694181 100644
--- a/src/main/java/baritone/behavior/LookBehavior.java
+++ b/src/main/java/baritone/behavior/LookBehavior.java
@@ -20,13 +20,11 @@ package baritone.behavior;
import baritone.Baritone;
import baritone.api.Settings;
import baritone.api.behavior.ILookBehavior;
-import baritone.api.event.events.PacketEvent;
-import baritone.api.event.events.PlayerUpdateEvent;
-import baritone.api.event.events.RotationMoveEvent;
-import baritone.api.event.events.WorldEvent;
-import baritone.api.utils.Helper;
+import baritone.api.behavior.look.IAimProcessor;
+import baritone.api.event.events.*;
import baritone.api.utils.IPlayerContext;
import baritone.api.utils.Rotation;
+import baritone.behavior.look.ForkableRandom;
import net.minecraft.network.play.client.CPacketPlayer;
import java.util.Optional;
@@ -44,14 +42,17 @@ public final class LookBehavior extends Behavior implements ILookBehavior {
private Rotation serverRotation;
/**
- * The last player rotation. Used when free looking
+ * The last player rotation. Used to restore the player's angle when using free look.
*
* @see Settings#freeLook
*/
private Rotation prevRotation;
+ private final AimProcessor processor;
+
public LookBehavior(Baritone baritone) {
super(baritone);
+ this.processor = new AimProcessor(baritone.getPlayerContext());
}
@Override
@@ -59,6 +60,19 @@ public final class LookBehavior extends Behavior implements ILookBehavior {
this.target = new Target(rotation, blockInteract);
}
+ @Override
+ public IAimProcessor getAimProcessor() {
+ return this.processor;
+ }
+
+ @Override
+ public void onTick(TickEvent event) {
+ if (event.getType() == TickEvent.Type.IN) {
+ // Unlike forked AimProcessors, the root one needs to be manually updated each game tick
+ this.processor.tick();
+ }
+ }
+
@Override
public void onPlayerUpdate(PlayerUpdateEvent event) {
if (this.target == null) {
@@ -67,34 +81,16 @@ public final class LookBehavior extends Behavior implements ILookBehavior {
switch (event.getState()) {
case PRE: {
if (this.target.mode == Target.Mode.NONE) {
+ // Just return for PRE, we still want to set target to null on POST
return;
}
if (this.target.mode == Target.Mode.SERVER) {
this.prevRotation = new Rotation(ctx.player().rotationYaw, ctx.player().rotationPitch);
}
- final float oldYaw = ctx.playerRotations().getYaw();
- final float oldPitch = ctx.playerRotations().getPitch();
-
- float desiredYaw = this.target.rotation.getYaw();
- float desiredPitch = this.target.rotation.getPitch();
-
- // In other words, the target doesn't care about the pitch, so it used playerRotations().getPitch()
- // and it's safe to adjust it to a normal level
- if (desiredPitch == oldPitch) {
- desiredPitch = nudgeToLevel(desiredPitch);
- }
-
- desiredYaw += (Math.random() - 0.5) * Baritone.settings().randomLooking.value;
- desiredPitch += (Math.random() - 0.5) * Baritone.settings().randomLooking.value;
-
- ctx.player().rotationYaw = calculateMouseMove(oldYaw, desiredYaw);
- ctx.player().rotationPitch = calculateMouseMove(oldPitch, desiredPitch);
-
- if (this.target.mode == Target.Mode.CLIENT) {
- // The target can be invalidated now since it won't be needed for RotationMoveEvent
- this.target = null;
- }
+ final Rotation actual = this.processor.nextRotation(this.target.rotation);
+ ctx.player().rotationYaw = actual.getYaw();
+ ctx.player().rotationPitch = actual.getPitch();
break;
}
case POST: {
@@ -133,7 +129,8 @@ public final class LookBehavior extends Behavior implements ILookBehavior {
public void pig() {
if (this.target != null) {
- ctx.player().rotationYaw = this.target.rotation.getYaw();
+ final Rotation actual = this.processor.nextRotation(this.target.rotation);
+ ctx.player().rotationYaw = actual.getYaw();
}
}
@@ -148,37 +145,124 @@ public final class LookBehavior extends Behavior implements ILookBehavior {
@Override
public void onPlayerRotationMove(RotationMoveEvent event) {
if (this.target != null) {
- event.setYaw(this.target.rotation.getYaw());
- event.setPitch(this.target.rotation.getPitch());
+ final Rotation actual = this.target.rotation;
+ event.setYaw(actual.getYaw());
+ event.setPitch(actual.getPitch());
}
}
- /**
- * Nudges the player's pitch to a regular level. (Between {@code -20} and {@code 10}, increments are by {@code 1})
- */
- private static float nudgeToLevel(float pitch) {
- if (pitch < -20) {
- return pitch + 1;
- } else if (pitch > 10) {
- return pitch - 1;
+ private static final class AimProcessor extends AbstractAimProcessor {
+
+ public AimProcessor(final IPlayerContext ctx) {
+ super(ctx);
+ }
+
+ @Override
+ protected Rotation getPrevRotation() {
+ // Implementation will use LookBehavior.serverRotation
+ return ctx.playerRotations();
}
- return pitch;
}
- private float calculateMouseMove(float current, float target) {
- final float delta = target - current;
- final int deltaPx = angleToMouse(delta);
- return current + mouseToAngle(deltaPx);
- }
+ private static abstract class AbstractAimProcessor implements IAimProcessor {
- private int angleToMouse(float angleDelta) {
- final float minAngleChange = mouseToAngle(1);
- return Math.round(angleDelta / minAngleChange);
- }
+ protected final IPlayerContext ctx;
+ private final ForkableRandom rand;
+ private double randomYawOffset;
+ private double randomPitchOffset;
- private float mouseToAngle(int mouseDelta) {
- final float f = ctx.minecraft().gameSettings.mouseSensitivity * 0.6f + 0.2f;
- return mouseDelta * f * f * f * 8.0f * 0.15f;
+ public AbstractAimProcessor(IPlayerContext ctx) {
+ this.ctx = ctx;
+ this.rand = new ForkableRandom();
+ }
+
+ private AbstractAimProcessor(final AbstractAimProcessor source) {
+ this.ctx = source.ctx;
+ this.rand = source.rand.fork();
+ this.randomYawOffset = source.randomYawOffset;
+ this.randomPitchOffset = source.randomPitchOffset;
+ }
+
+ public void tick() {
+ this.randomYawOffset = (this.rand.nextDouble() - 0.5) * Baritone.settings().randomLooking.value;
+ this.randomPitchOffset = (this.rand.nextDouble() - 0.5) * Baritone.settings().randomLooking.value;
+ }
+
+ @Override
+ public Rotation nextRotation(final Rotation rotation) {
+ final Rotation prev = this.getPrevRotation();
+
+ float desiredYaw = rotation.getYaw();
+ float desiredPitch = rotation.getPitch();
+
+ // In other words, the target doesn't care about the pitch, so it used playerRotations().getPitch()
+ // and it's safe to adjust it to a normal level
+ if (desiredPitch == prev.getPitch()) {
+ desiredPitch = nudgeToLevel(desiredPitch);
+ }
+
+ desiredYaw += this.randomYawOffset;
+ desiredPitch += this.randomPitchOffset;
+
+ return new Rotation(
+ this.calculateMouseMove(prev.getYaw(), desiredYaw),
+ this.calculateMouseMove(prev.getPitch(), desiredPitch)
+ );
+ }
+
+ @Override
+ public final IAimProcessor fork(final int ticksAdvanced) {
+ final AbstractAimProcessor fork = new AbstractAimProcessor(this) {
+
+ private Rotation prev = AbstractAimProcessor.this.getPrevRotation();
+
+ @Override
+ public Rotation nextRotation(Rotation rotation) {
+ final Rotation actual = super.nextRotation(rotation);
+ this.tick();
+ return (this.prev = actual);
+ }
+
+ @Override
+ protected Rotation getPrevRotation() {
+ return this.prev;
+ }
+ };
+ for (int i = 0; i < ticksAdvanced; i++) {
+ fork.tick();
+ }
+ return fork;
+ }
+
+ protected abstract Rotation getPrevRotation();
+
+ /**
+ * Nudges the player's pitch to a regular level. (Between {@code -20} and {@code 10}, increments are by {@code 1})
+ */
+ private float nudgeToLevel(float pitch) {
+ if (pitch < -20) {
+ return pitch + 1;
+ } else if (pitch > 10) {
+ return pitch - 1;
+ }
+ return pitch;
+ }
+
+ private float calculateMouseMove(float current, float target) {
+ final float delta = target - current;
+ final int deltaPx = angleToMouse(delta);
+ return current + mouseToAngle(deltaPx);
+ }
+
+ private int angleToMouse(float angleDelta) {
+ final float minAngleChange = mouseToAngle(1);
+ return Math.round(angleDelta / minAngleChange);
+ }
+
+ private float mouseToAngle(int mouseDelta) {
+ final float f = ctx.minecraft().gameSettings.mouseSensitivity * 0.6f + 0.2f;
+ return mouseDelta * f * f * f * 8.0f * 0.15f;
+ }
}
private static class Target {
diff --git a/src/main/java/baritone/behavior/look/ForkableRandom.java b/src/main/java/baritone/behavior/look/ForkableRandom.java
new file mode 100644
index 000000000..5f5f942d2
--- /dev/null
+++ b/src/main/java/baritone/behavior/look/ForkableRandom.java
@@ -0,0 +1,85 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone. If not, see .
+ */
+
+package baritone.behavior.look;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.LongSupplier;
+
+/**
+ * Implementation of Xoroshiro256++
+ *
+ * Extended to produce random double-precision floating point numbers, and allow copies to be spawned via {@link #fork},
+ * which share the same internal state as the source object.
+ *
+ * @author Brady
+ */
+public final class ForkableRandom {
+
+ private static final double DOUBLE_UNIT = 0x1.0p-53;
+
+ private final long[] s;
+
+ public ForkableRandom() {
+ this(System.nanoTime() ^ System.currentTimeMillis());
+ }
+
+ public ForkableRandom(long seedIn) {
+ final AtomicLong seed = new AtomicLong(seedIn);
+ final LongSupplier splitmix64 = () -> {
+ long z = seed.addAndGet(0x9e3779b97f4a7c15L);
+ z = (z ^ (z >>> 30)) * 0xbf58476d1ce4e5b9L;
+ z = (z ^ (z >>> 27)) * 0x94d049bb133111ebL;
+ return z ^ (z >>> 31);
+ };
+ this.s = new long[] {
+ splitmix64.getAsLong(),
+ splitmix64.getAsLong(),
+ splitmix64.getAsLong(),
+ splitmix64.getAsLong()
+ };
+ }
+
+ private ForkableRandom(long[] s) {
+ this.s = s;
+ }
+
+ public double nextDouble() {
+ return (this.next() >>> 11) * DOUBLE_UNIT;
+ }
+
+ public long next() {
+ final long result = rotl(this.s[0] + this.s[3], 23) + this.s[0];
+ final long t = this.s[1] << 17;
+ this.s[2] ^= this.s[0];
+ this.s[3] ^= this.s[1];
+ this.s[1] ^= this.s[2];
+ this.s[0] ^= this.s[3];
+ this.s[2] ^= t;
+ this.s[3] = rotl(this.s[3], 45);
+ return result;
+ }
+
+ public ForkableRandom fork() {
+ return new ForkableRandom(Arrays.copyOf(this.s, 4));
+ }
+
+ private static long rotl(long x, int k) {
+ return (x << k) | (x >>> (64 - k));
+ }
+}