diff --git a/src/client/java/skybot/SkybotClient.java b/src/client/java/skybot/SkybotClient.java index 319f0a3..3d3a0d8 100644 --- a/src/client/java/skybot/SkybotClient.java +++ b/src/client/java/skybot/SkybotClient.java @@ -71,15 +71,14 @@ public class SkybotClient implements ClientModInitializer { TASK_QUEUE.tick(); - float delta = client.getRenderTickCounter().getTickProgress(false); - - Controller.tickLook(client, delta); + // float delta = client.getRenderTickCounter().getTickProgress(false); + // Controller.tickLook(client, delta); } }); ClientTickEvents.END_CLIENT_TICK.register(client -> { if (client.world != null && !client.isPaused()) { - Controller.tickInput(client); + Controller.tick(client); while (STOP_KEY.wasPressed()) { if (botEnabled) { diff --git a/src/client/java/skybot/controller/Controller.java b/src/client/java/skybot/controller/Controller.java index a7f5c73..cd700f6 100644 --- a/src/client/java/skybot/controller/Controller.java +++ b/src/client/java/skybot/controller/Controller.java @@ -1,10 +1,13 @@ package skybot.controller; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Consumer; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.option.GameOptions; import net.minecraft.client.option.KeyBinding; -import net.minecraft.util.math.MathHelper; +import net.minecraft.client.util.InputUtil; /** * Owns simulated input state (movement keys + look direction) and applies it @@ -23,9 +26,14 @@ public class Controller { private final static KeyOverride attack = new KeyOverride(); // Left click private final static KeyOverride use = new KeyOverride(); // Right click - private static Double targetYaw = null; - private static Double targetPitch = null; - private static double turnSpeed = 10.0; // max degrees per tick + // Overrides for arbitrary physical keys, keyed by whatever KeyBinding(s) the + // player currently has bound to that key (same dispatch GLFW's real key + // callback uses), so any bindable key works without a dedicated field. + private final static Map keyOverrides = new HashMap<>(); + + // private static Double targetYaw = null; + // private static Double targetPitch = null; + // private static double turnSpeed = 10.0; // max degrees per tick public static void setForward(Boolean pressed) { forward.set(pressed); @@ -63,20 +71,44 @@ public class Controller { use.set(pressed); } - public static void lookAt(double yaw, double pitch) { - targetYaw = yaw; - targetPitch = MathHelper.clamp(pitch, -90.0, 90.0); + /** + * Overrides an arbitrary physical key, e.g. + * Controller.setKey(InputUtil.GLFW_KEY_KP_7, true). + */ + public static void setKey(InputUtil.Key key, Boolean pressed) { + keyOverrides.computeIfAbsent(key, k -> new KeyOverride()).set(pressed); } - public static void setTurnSpeed(double degreesPerTick) { - turnSpeed = degreesPerTick; + /** + * Overrides an arbitrary physical key by its GLFW key code, e.g. + * InputUtil.GLFW_KEY_KP_7. + */ + public static void setKey(int glfwKeyCode, Boolean pressed) { + setKey(InputUtil.Type.KEYSYM.createFromCode(glfwKeyCode), pressed); } - public static void clearLook() { - targetYaw = null; - targetPitch = null; + /** + * A setter bound to one physical key, e.g. + * new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_KP_7)). + */ + public static Consumer key(int glfwKeyCode) { + return pressed -> setKey(glfwKeyCode, pressed); } + // public static void lookAt(double yaw, double pitch) { + // targetYaw = yaw; + // targetPitch = MathHelper.clamp(pitch, -90.0, 90.0); + // } + + // public static void setTurnSpeed(double degreesPerTick) { + // turnSpeed = degreesPerTick; + // } + + // public static void clearLook() { + // targetYaw = null; + // targetPitch = null; + // } + /** * Clears every override, handing all input back to the player. Call on * disconnect. @@ -91,28 +123,45 @@ public class Controller { sneaking.set(null); attack.set(null); use.set(null); - clearLook(); + + for (InputUtil.Key key : keyOverrides.keySet()) { + KeyBinding.setKeyPressed(key, false); + } + keyOverrides.clear(); + // clearLook(); } /** * Run this inside ClientTickEvents.END_CLIENT_TICK (20Hz). * Handles keyboard state overrides aligned with game engine updates. */ - public static void tickInput(MinecraftClient client) { + public static void tick(MinecraftClient client) { if (client.player == null) return; GameOptions options = client.options; - forward.apply(options.forwardKey); - back.apply(options.backKey); - left.apply(options.leftKey); - right.apply(options.rightKey); - jumping.apply(options.jumpKey); - sprinting.apply(options.sprintKey); - sneaking.apply(options.sneakKey); + forward.apply(options.forwardKey::setPressed); + back.apply(options.backKey::setPressed); + left.apply(options.leftKey::setPressed); + right.apply(options.rightKey::setPressed); + jumping.apply(options.jumpKey::setPressed); + sprinting.apply(options.sprintKey::setPressed); + sneaking.apply(options.sneakKey::setPressed); - attack.apply(options.attackKey); - use.apply(options.useKey); + attack.apply(options.attackKey::setPressed); + use.apply(options.useKey::setPressed); + + for (Iterator> it = keyOverrides.entrySet().iterator(); it + .hasNext();) { + Map.Entry entry = it.next(); + InputUtil.Key key = entry.getKey(); + KeyOverride override = entry.getValue(); + + override.apply(pressed -> KeyBinding.setKeyPressed(key, pressed)); + if (override.isIdle()) { + it.remove(); + } + } } /** @@ -122,30 +171,34 @@ public class Controller { * @param tickDelta The fractional time elapsed since the last game tick (0.0 - * 1.0) */ - public static void tickLook(MinecraftClient client, float tickDelta) { - ClientPlayerEntity player = client.player; - if (player == null || targetYaw == null || targetPitch == null) - return; + // public static void tickLook(MinecraftClient client, float tickDelta) { + // ClientPlayerEntity player = client.player; + // if (player == null || targetYaw == null || targetPitch == null) + // return; - double yawDiff = MathHelper.wrapDegrees(targetYaw - player.getYaw()); - double pitchDiff = targetPitch - player.getPitch(); + // double yawDiff = MathHelper.wrapDegrees(targetYaw - player.getYaw()); + // double pitchDiff = targetPitch - player.getPitch(); - // Scale turnSpeed using tickDelta so aiming remains consistent regardless of - // FPS - double scaledTurnSpeed = turnSpeed * tickDelta; + // // Scale turnSpeed using tickDelta so aiming remains consistent regardless of + // // FPS + // double scaledTurnSpeed = turnSpeed * tickDelta; - // Fallback case: if tickDelta is 0 (e.g., game paused or frame drop), fallback - // to a tiny step - if (scaledTurnSpeed <= 0) { - scaledTurnSpeed = turnSpeed * 0.1; - } + // // Fallback case: if tickDelta is 0 (e.g., game paused or frame drop), + // fallback + // // to a tiny step + // if (scaledTurnSpeed <= 0) { + // scaledTurnSpeed = turnSpeed * 0.1; + // } - double yawStep = MathHelper.clamp(yawDiff, -scaledTurnSpeed, scaledTurnSpeed); - double pitchStep = MathHelper.clamp(pitchDiff, -scaledTurnSpeed, scaledTurnSpeed); + // double yawStep = MathHelper.clamp(yawDiff, -scaledTurnSpeed, + // scaledTurnSpeed); + // double pitchStep = MathHelper.clamp(pitchDiff, -scaledTurnSpeed, + // scaledTurnSpeed); - player.setYaw((float) (player.getYaw() + yawStep)); - player.setPitch((float) MathHelper.clamp(player.getPitch() + pitchStep, -90.0, 90.0)); - } + // player.setYaw((float) (player.getYaw() + yawStep)); + // player.setPitch((float) MathHelper.clamp(player.getPitch() + pitchStep, + // -90.0, 90.0)); + // } /** * Tracks one key's override state. null means "no override, leave real input @@ -163,14 +216,19 @@ public class Controller { state = pressed; } - void apply(KeyBinding binding) { + void apply(Consumer sink) { if (state != null) { - binding.setPressed(state); + sink.accept(state); controlled = true; } else if (controlled) { - binding.setPressed(false); + sink.accept(false); controlled = false; } } + + /** True once released and the forced-up frame has already been sent. */ + boolean isIdle() { + return state == null && !controlled; + } } } diff --git a/src/client/java/skybot/controller/task/ClickButtonTask.java b/src/client/java/skybot/controller/task/ClickButtonTask.java new file mode 100644 index 0000000..902d769 --- /dev/null +++ b/src/client/java/skybot/controller/task/ClickButtonTask.java @@ -0,0 +1,51 @@ +package skybot.controller.task; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; + +/** Clicks a button (press then release) held for a log-normally distributed duration. */ +public class ClickButtonTask implements Task { + private final Consumer setter; + private long endTime; + + public ClickButtonTask(Consumer setter) { + this.setter = setter; + } + + @Override + public void start() { + endTime = System.currentTimeMillis() + sampleLogNormalDistribution(); + setter.accept(true); + } + + @Override + public void tick() { + } + + @Override + public boolean isDone() { + if (endTime == 0) { + return false; + } + + return System.currentTimeMillis() >= endTime; + } + + @Override + public void stop() { + setter.accept(false); + } + + private long sampleLogNormalDistribution() { + long physicalLimitShift = 30; + + // Adjust mu so our median tail addition is roughly 50ms + // (30ms baseline + 50ms average tail = ~80ms human click hold time) + double mu = Math.log(50); + double sigma = 0.35; // Slightly wider variance for natural pacing inconsistencies + + long randomTail = (long) Math.exp(ThreadLocalRandom.current().nextGaussian() * sigma + mu); + + return physicalLimitShift + randomTail; + } +} diff --git a/src/client/java/skybot/controller/task/Task.java b/src/client/java/skybot/controller/task/Task.java index 7ba0736..adbe660 100644 --- a/src/client/java/skybot/controller/task/Task.java +++ b/src/client/java/skybot/controller/task/Task.java @@ -13,4 +13,9 @@ public interface Task { /** Called once, right after isDone() first returns true, before the next task starts. */ default void stop() { } + + /** Short name for this task, used for logging (e.g. via ChatMenu). */ + default String name() { + return getClass().getSimpleName(); + } } diff --git a/src/client/java/skybot/controller/task/TaskQueue.java b/src/client/java/skybot/controller/task/TaskQueue.java index 8e0920f..acbd012 100644 --- a/src/client/java/skybot/controller/task/TaskQueue.java +++ b/src/client/java/skybot/controller/task/TaskQueue.java @@ -19,6 +19,9 @@ public class TaskQueue { // Track the exact random delay we rolled for the active task private long rolledDelay = 0; + // Track when the active task actually started, to report how long it ran + private long taskStartTime = 0; + public TaskQueue add(Task task) { pending.add(task); return this; @@ -27,6 +30,10 @@ public class TaskQueue { public void tick() { if (current != null && current.isDone()) { current.stop(); + if (currentStarted) { + ChatMenu.send( + String.format("%s finished in %dms", current.name(), System.currentTimeMillis() - taskStartTime)); + } current = null; currentStarted = false; } @@ -46,10 +53,11 @@ public class TaskQueue { long totalDelta = rolledDelay + actualOverhead; ChatMenu.send( - String.format("Starting new job... (delayUntil: %dms)", totalDelta)); + String.format("Starting %s... (delayUntil: %dms)", current.name(), totalDelta)); current.start(); currentStarted = true; + taskStartTime = System.currentTimeMillis(); } current.tick(); diff --git a/src/client/java/skybot/farmers/CarrotFarmer.java b/src/client/java/skybot/farmers/CarrotFarmer.java index 869e16a..92562af 100644 --- a/src/client/java/skybot/farmers/CarrotFarmer.java +++ b/src/client/java/skybot/farmers/CarrotFarmer.java @@ -1,6 +1,8 @@ package skybot.farmers; +import net.minecraft.client.util.InputUtil; import skybot.controller.Controller; +import skybot.controller.task.ClickButtonTask; import skybot.controller.task.HoldKeyTask; import skybot.controller.task.PauseTask; import skybot.controller.task.PressButtonTask; @@ -13,29 +15,40 @@ public class CarrotFarmer implements Farmer { public void enqueue(TaskQueue queue) { queue.clear(); - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new HoldKeyTask(Controller::setLeft, 119555)); - queue.add(new ReleaseButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(2000)); + queue.add(new PauseTask(1998)); + queue.add(new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_KP_7))); - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new HoldKeyTask(Controller::setRight, 119202)); - queue.add(new ReleaseButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(2000)); + int EPOCHS = 2; - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new HoldKeyTask(Controller::setLeft, 112023)); - queue.add(new ReleaseButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(2000)); + for (int i = 0; i < EPOCHS; i++) { + queue.add(new PauseTask(2021)); - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new HoldKeyTask(Controller::setRight, 119355)); - queue.add(new ReleaseButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(2000)); + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setLeft, 119555)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2015)); - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new HoldKeyTask(Controller::setLeft, 120155)); - queue.add(new ReleaseButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(2000)); + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setRight, 119402)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(1998)); + + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setLeft, 119223)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2203)); + + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setRight, 119198)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(1998)); + + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setLeft, 120155)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2126)); + + queue.add(new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_KP_7))); + } } } diff --git a/src/client/java/skybot/farmers/TestFarmer.java b/src/client/java/skybot/farmers/TestFarmer.java index ee9075a..bd14e2e 100644 --- a/src/client/java/skybot/farmers/TestFarmer.java +++ b/src/client/java/skybot/farmers/TestFarmer.java @@ -1,8 +1,9 @@ package skybot.farmers; +import net.minecraft.client.util.InputUtil; import skybot.controller.Controller; +import skybot.controller.task.ClickButtonTask; import skybot.controller.task.PauseTask; -import skybot.controller.task.PressButtonTask; import skybot.controller.task.TaskQueue; public class TestFarmer implements Farmer { @@ -10,7 +11,8 @@ public class TestFarmer implements Farmer { @Override public void enqueue(TaskQueue queue) { queue.clear(); - queue.add(new PressButtonTask(Controller::setLeftClick)); - queue.add(new PauseTask(5000)); + queue.add(new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_D))); + queue.add(new PauseTask(2000)); + queue.add(new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_D))); } }