@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user