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)); + } +}