diff --git a/src/client/java/skybot/SkybotClient.java b/src/client/java/skybot/SkybotClient.java index ade59c7..319f0a3 100644 --- a/src/client/java/skybot/SkybotClient.java +++ b/src/client/java/skybot/SkybotClient.java @@ -1,59 +1,99 @@ package skybot; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; - import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.minecraft.client.network.ServerInfo; +import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.util.Identifier; +import org.lwjgl.glfw.GLFW; + +import skybot.commands.CommandManager; import skybot.controller.Controller; -import skybot.controller.task.HoldKeyTask; -import skybot.controller.task.PauseTask; import skybot.controller.task.TaskQueue; +import skybot.farmers.Farmer; import skybot.utilities.ChatMenu; public class SkybotClient implements ClientModInitializer { - public static final String HYPIXEL_URL = "hypixel.net"; private static final TaskQueue TASK_QUEUE = new TaskQueue(); + private static volatile Farmer activeFarmer = null; + private static volatile boolean botEnabled = false; + + private static final KeyBinding STOP_KEY = new KeyBinding( + "key.skybot.stop", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_CAPS_LOCK, + KeyBinding.Category.create(Identifier.of("skybot", "general"))); + + public static boolean isBotEnabled() { + return botEnabled; + } + + public static void stopBot() { + activeFarmer = null; + botEnabled = false; + TASK_QUEUE.clear(); + Controller.releaseAll(); + } + + public static boolean startBot(Farmer strategy) { + if (isBotEnabled()) { + ChatMenu.send("§cFailed to start: Another routine is already running.§r"); + return false; + } + + stopBot(); + + activeFarmer = strategy; + botEnabled = true; + + activeFarmer.enqueue(TASK_QUEUE); + ChatMenu.send("§aFarmer started successfully.§r"); + return true; + } @Override public void onInitializeClient() { + KeyBindingHelper.registerKeyBinding(STOP_KEY); + + WorldRenderEvents.END_MAIN.register(context -> { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world != null && !client.isPaused()) { + if (TASK_QUEUE.isEmpty() && botEnabled) { + ChatMenu.send("Farmer is idle."); + stopBot(); + } + + TASK_QUEUE.tick(); + + float delta = client.getRenderTickCounter().getTickProgress(false); + + Controller.tickLook(client, delta); + } + }); + ClientTickEvents.END_CLIENT_TICK.register(client -> { - TASK_QUEUE.tick(); - Controller.tick(client); + if (client.world != null && !client.isPaused()) { + Controller.tickInput(client); + + while (STOP_KEY.wasPressed()) { + if (botEnabled) { + ChatMenu.send("§cBot stopped via hotkey.§r"); + stopBot(); + } + } + } }); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { - dispatcher.register(literal("test").executes(context -> { - TASK_QUEUE.clear(); - TASK_QUEUE.add(new HoldKeyTask(Controller::setForward, 200)); - TASK_QUEUE.add(new PauseTask(400)); - TASK_QUEUE.add(new HoldKeyTask(Controller::setRight, 400)); - ChatMenu.send("running: forward 2s -> pause 4s -> right 4s"); - return 1; - })); - }); - - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { - client.execute(() -> { - if (client.player == null) - return; - - ServerInfo server = client.getCurrentServerEntry(); - if (server == null || !server.address.contains(HYPIXEL_URL)) - return; - }); - }); + ClientCommandRegistrationCallback.EVENT.register(CommandManager::register); ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { - TASK_QUEUE.clear(); - Controller.releaseAll(); - - ServerInfo server = client.getCurrentServerEntry(); - if (server == null || !server.address.contains(HYPIXEL_URL)) - return; + stopBot(); }); } } diff --git a/src/client/java/skybot/commands/CommandManager.java b/src/client/java/skybot/commands/CommandManager.java new file mode 100644 index 0000000..17e0ad8 --- /dev/null +++ b/src/client/java/skybot/commands/CommandManager.java @@ -0,0 +1,49 @@ +package skybot.commands; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +import com.mojang.brigadier.CommandDispatcher; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.command.CommandRegistryAccess; +import skybot.SkybotClient; +import skybot.farmers.CarrotFarmer; +import skybot.farmers.TestFarmer; +import skybot.utilities.ChatMenu; + +public class CommandManager { + + public static void register( + CommandDispatcher dispatcher, + CommandRegistryAccess registryAccess) { + + dispatcher.register(literal("farm") + .then(literal("test").executes(context -> { + if (SkybotClient.startBot(new TestFarmer())) { + ChatMenu.send("§aStarting Test Farmer...§r"); + } + return 1; + })) + .then(literal("carrots").executes(context -> { + if (SkybotClient.startBot(new CarrotFarmer())) { + ChatMenu.send("§aStarting Carrot Farmer...§r"); + } + return 1; + }))); + + dispatcher.register(literal("skybot") + .then(literal("status").executes(context -> { + String msg = SkybotClient.isBotEnabled() ? "§aRunning" : "§cIdle"; + ChatMenu.send("Status: " + msg); + return 1; + })) + .then(literal("stop").executes(context -> { + SkybotClient.stopBot(); + ChatMenu.send("§cBot stopped immediately.§r"); + return 1; + }))); + + // You can easily add more commands here as your framework expands: + // CropSelectionCommand.register(dispatcher); + // ConfigCommand.register(dispatcher); + } +} \ No newline at end of file diff --git a/src/client/java/skybot/controller/Controller.java b/src/client/java/skybot/controller/Controller.java index fec1aea..a7f5c73 100644 --- a/src/client/java/skybot/controller/Controller.java +++ b/src/client/java/skybot/controller/Controller.java @@ -20,6 +20,9 @@ public class Controller { private final static KeyOverride sprinting = new KeyOverride(); private final static KeyOverride sneaking = new KeyOverride(); + 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 @@ -52,6 +55,14 @@ public class Controller { sneaking.set(pressed); } + public static void setLeftClick(Boolean pressed) { + attack.set(pressed); + } + + public static void setRightClick(Boolean pressed) { + use.set(pressed); + } + public static void lookAt(double yaw, double pitch) { targetYaw = yaw; targetPitch = MathHelper.clamp(pitch, -90.0, 90.0); @@ -66,7 +77,10 @@ public class Controller { targetPitch = null; } - /** Clears every override, handing all input back to the player. Call on disconnect. */ + /** + * Clears every override, handing all input back to the player. Call on + * disconnect. + */ public static void releaseAll() { forward.set(null); back.set(null); @@ -75,12 +89,17 @@ public class Controller { jumping.set(null); sprinting.set(null); sneaking.set(null); + attack.set(null); + use.set(null); clearLook(); } - public static void tick(MinecraftClient client) { - ClientPlayerEntity player = client.player; - if (player == null) + /** + * Run this inside ClientTickEvents.END_CLIENT_TICK (20Hz). + * Handles keyboard state overrides aligned with game engine updates. + */ + public static void tickInput(MinecraftClient client) { + if (client.player == null) return; GameOptions options = client.options; @@ -92,26 +111,49 @@ public class Controller { sprinting.apply(options.sprintKey); sneaking.apply(options.sneakKey); - if (targetYaw != null && targetPitch != null) { - applyLook(player); - } + attack.apply(options.attackKey); + use.apply(options.useKey); } - private static void applyLook(ClientPlayerEntity player) { + /** + * Run this inside WorldRenderEvents.END_MAIN (Every Frame). + * Handles sub-tick camera adjustments smoothly using frame delta time. + * + * @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; + double yawDiff = MathHelper.wrapDegrees(targetYaw - player.getYaw()); double pitchDiff = targetPitch - player.getPitch(); - double yawStep = MathHelper.clamp(yawDiff, -turnSpeed, turnSpeed); - double pitchStep = MathHelper.clamp(pitchDiff, -turnSpeed, turnSpeed); + // 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; + } + + 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)); } /** - * Tracks one key's override state. null means "no override, leave real input alone." - * When an override is released (goes back to null), forces the key up exactly once - * so it doesn't stay latched from the last forced value, then stops touching it. + * Tracks one key's override state. null means "no override, leave real input + * alone." + * When an override is released (goes back to null), forces the key up exactly + * once + * so it doesn't stay latched from the last forced value, then stops touching + * it. */ private static class KeyOverride { private Boolean state; diff --git a/src/client/java/skybot/controller/task/PressButtonTask.java b/src/client/java/skybot/controller/task/PressButtonTask.java new file mode 100644 index 0000000..3c30756 --- /dev/null +++ b/src/client/java/skybot/controller/task/PressButtonTask.java @@ -0,0 +1,31 @@ +package skybot.controller.task; + +import java.util.function.Consumer; + +public class PressButtonTask implements Task { + private final Consumer setter; + private boolean pressed = false; + + public PressButtonTask(Consumer setter) { + this.setter = setter; + } + + @Override + public void start() { + setter.accept(true); + pressed = true; + } + + @Override + public void tick() { + } + + @Override + public void stop() { + } + + @Override + public boolean isDone() { + return pressed; + } +} diff --git a/src/client/java/skybot/controller/task/ReleaseButtonTask.java b/src/client/java/skybot/controller/task/ReleaseButtonTask.java new file mode 100644 index 0000000..1b2d0c6 --- /dev/null +++ b/src/client/java/skybot/controller/task/ReleaseButtonTask.java @@ -0,0 +1,32 @@ +package skybot.controller.task; + +import java.util.function.Consumer; + +public class ReleaseButtonTask implements Task { + + private final Consumer setter; + private boolean released = false; + + public ReleaseButtonTask(Consumer setter) { + this.setter = setter; + } + + @Override + public void start() { + setter.accept(false); + released = true; + } + + @Override + public void tick() { + } + + @Override + public void stop() { + } + + @Override + public boolean isDone() { + return released; + } +} diff --git a/src/client/java/skybot/controller/task/TaskQueue.java b/src/client/java/skybot/controller/task/TaskQueue.java index 9d7c214..8e0920f 100644 --- a/src/client/java/skybot/controller/task/TaskQueue.java +++ b/src/client/java/skybot/controller/task/TaskQueue.java @@ -34,8 +34,7 @@ public class TaskQueue { if (current == null) { current = pending.poll(); if (current != null) { - // Roll the random delay - rolledDelay = ThreadLocalRandom.current().nextLong(51, 200); + rolledDelay = sampleLogNormalDistribution(); delayUntil = System.currentTimeMillis() + rolledDelay; } } @@ -47,7 +46,7 @@ public class TaskQueue { long totalDelta = rolledDelay + actualOverhead; ChatMenu.send( - String.format("Starting new job... (Target delay: %dms, Actual delta: %dms)", rolledDelay, totalDelta)); + String.format("Starting new job... (delayUntil: %dms)", totalDelta)); current.start(); currentStarted = true; @@ -57,7 +56,7 @@ public class TaskQueue { } } - public boolean isDone() { + public boolean isEmpty() { return current == null && pending.isEmpty(); } @@ -67,4 +66,18 @@ public class TaskQueue { delayUntil = 0; rolledDelay = 0; } + + private long sampleLogNormalDistribution() { + long physicalLimitShift = 80; + + // Adjust mu so our median tail addition is roughly 70ms + // (130ms baseline + 70ms average tail = ~200ms real-world human action speed) + double mu = Math.log(70); + double sigma = 0.35; // Slightly wider variance for natural pacing inconsistencies + + long randomTail = (long) Math.exp(ThreadLocalRandom.current().nextGaussian() * sigma + mu); + + // Combine them: your delays will smoothly scale from 140ms up to 350ms+ + return physicalLimitShift + randomTail; + } } \ No newline at end of file diff --git a/src/client/java/skybot/farmers/CarrotFarmer.java b/src/client/java/skybot/farmers/CarrotFarmer.java new file mode 100644 index 0000000..869e16a --- /dev/null +++ b/src/client/java/skybot/farmers/CarrotFarmer.java @@ -0,0 +1,41 @@ +package skybot.farmers; + +import skybot.controller.Controller; +import skybot.controller.task.HoldKeyTask; +import skybot.controller.task.PauseTask; +import skybot.controller.task.PressButtonTask; +import skybot.controller.task.ReleaseButtonTask; +import skybot.controller.task.TaskQueue; + +public class CarrotFarmer implements Farmer { + + @Override + 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 PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setRight, 119202)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2000)); + + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new HoldKeyTask(Controller::setLeft, 112023)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2000)); + + 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, 120155)); + queue.add(new ReleaseButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(2000)); + } +} diff --git a/src/client/java/skybot/farmers/Farmer.java b/src/client/java/skybot/farmers/Farmer.java new file mode 100644 index 0000000..1f4865a --- /dev/null +++ b/src/client/java/skybot/farmers/Farmer.java @@ -0,0 +1,7 @@ +package skybot.farmers; + +import skybot.controller.task.TaskQueue; + +public interface Farmer { + void enqueue(TaskQueue queue); +} diff --git a/src/client/java/skybot/farmers/TestFarmer.java b/src/client/java/skybot/farmers/TestFarmer.java new file mode 100644 index 0000000..ee9075a --- /dev/null +++ b/src/client/java/skybot/farmers/TestFarmer.java @@ -0,0 +1,16 @@ +package skybot.farmers; + +import skybot.controller.Controller; +import skybot.controller.task.PauseTask; +import skybot.controller.task.PressButtonTask; +import skybot.controller.task.TaskQueue; + +public class TestFarmer implements Farmer { + + @Override + public void enqueue(TaskQueue queue) { + queue.clear(); + queue.add(new PressButtonTask(Controller::setLeftClick)); + queue.add(new PauseTask(5000)); + } +}