Carrot farmer!
build / build (push) Failing after 3s

This commit is contained in:
2026-07-04 04:10:02 -07:00
parent e629a14b08
commit 9ff19ae67b
9 changed files with 323 additions and 52 deletions
+73 -33
View File
@@ -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() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
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();
Controller.tick(client);
float delta = client.getRenderTickCounter().getTickProgress(false);
Controller.tickLook(client, delta);
}
});
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;
}));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.world != null && !client.isPaused()) {
Controller.tickInput(client);
while (STOP_KEY.wasPressed()) {
if (botEnabled) {
ChatMenu.send("§cBot stopped via hotkey.§r");
stopBot();
}
}
}
});
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();
});
}
}
@@ -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<FabricClientCommandSource> 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);
}
}
@@ -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;
@@ -0,0 +1,31 @@
package skybot.controller.task;
import java.util.function.Consumer;
public class PressButtonTask implements Task {
private final Consumer<Boolean> setter;
private boolean pressed = false;
public PressButtonTask(Consumer<Boolean> 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;
}
}
@@ -0,0 +1,32 @@
package skybot.controller.task;
import java.util.function.Consumer;
public class ReleaseButtonTask implements Task {
private final Consumer<Boolean> setter;
private boolean released = false;
public ReleaseButtonTask(Consumer<Boolean> 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;
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -0,0 +1,7 @@
package skybot.farmers;
import skybot.controller.task.TaskQueue;
public interface Farmer {
void enqueue(TaskQueue queue);
}
@@ -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));
}
}