Compare commits

..

13 Commits

Author SHA1 Message Date
burkkyy 990eb72753 dont disconnect
build / build (push) Successful in 2m8s
2026-07-04 21:58:03 -07:00
burkkyy 89a49e2d58 disconnect if healthcheck fails
build / build (push) Successful in 1m53s
2026-07-04 18:22:56 -07:00
burkkyy 5c94367584 healthcheck func impl 2026-07-04 18:12:16 -07:00
burkkyy b9869c57f2 Adding netherwart and wheat farmer
build / build (push) Successful in 2m1s
2026-07-04 16:50:02 -07:00
burkkyy 6a6b92a549 upload artifact version downgrade for gitea
build / build (push) Successful in 2m10s
2026-07-04 15:20:18 -07:00
burkkyy 8637b89668 fixing build workflow
build / build (push) Failing after 4m40s
2026-07-04 15:00:25 -07:00
burkkyy 431aae4f62 Moved task out of controller
build / build (push) Failing after 2s
2026-07-04 05:46:58 -07:00
burkkyy 2fba8b6299 adding potato farmer
build / build (push) Failing after 2s
2026-07-04 05:44:22 -07:00
burkkyy 39aeecad1a CarrotFarmer simplification
build / build (push) Failing after 2s
2026-07-04 05:41:33 -07:00
burkkyy 18d594c02c New Run Command Task
build / build (push) Failing after 2s
2026-07-04 05:34:38 -07:00
burkkyy c534cc67a8 TaskQueue fix 2026-07-04 05:34:29 -07:00
burkkyy c3bbf9af4b Big upgrades
build / build (push) Failing after 1s
2026-07-04 05:10:54 -07:00
burkkyy 9ff19ae67b Carrot farmer!
build / build (push) Failing after 3s
2026-07-04 04:10:02 -07:00
20 changed files with 856 additions and 114 deletions
+11 -6
View File
@@ -11,20 +11,25 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
- name: validate gradle wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@v4.3.1
- name: setup jdk
uses: actions/setup-java@v4
uses: actions/setup-java@v4.7.1
with:
java-version: '25'
distribution: 'microsoft'
java-version: "25"
distribution: "microsoft"
- name: make gradle wrapper executable
run: chmod +x ./gradlew
- name: build
run: ./gradlew build
- name: capture build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3.1.3
with:
name: Artifacts
path: build/libs/
+129 -35
View File
@@ -1,59 +1,153 @@
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.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
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.task.TaskQueue;
import skybot.utilities.ChatMenu;
import skybot.utilities.TickScheduler;
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 long tickCount = 0;
private static final long HEALTHCHECK_INTERVAL_TICKS = 40;
private static volatile long lastBreakTime = 0;
private static volatile long lastMoveTime = 0;
private static Vec3d lastPosition = null;
private static final double MOVE_EPSILON_SQUARED = 1.0;
private static final KeyBinding.Category GENERAL_CATEGORY = KeyBinding.Category
.create(Identifier.of("skybot", "general"));
private static final KeyBinding STOP_KEY = new KeyBinding(
"key.skybot.stop",
InputUtil.Type.KEYSYM,
GLFW.GLFW_KEY_CAPS_LOCK,
GENERAL_CATEGORY);
public static boolean isBotEnabled() {
return botEnabled;
}
public static long getLastBreakTime() {
return lastBreakTime;
}
public static long getLastMoveTime() {
return lastMoveTime;
}
public static String botStatus() {
if (!botEnabled) {
return "§cIdle";
}
return "§aRunning";
}
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;
lastBreakTime = System.currentTimeMillis();
lastMoveTime = System.currentTimeMillis();
lastPosition = null;
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);
TickScheduler.register();
ClientPlayerBlockBreakEvents.AFTER
.register((world, player, pos, state) -> lastBreakTime = System.currentTimeMillis());
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 -> {
if (client.world != null && !client.isPaused()) {
Controller.tick(client);
if (client.player != null) {
Vec3d pos = client.player.getEntityPos();
if (lastPosition == null) {
lastPosition = pos;
lastMoveTime = System.currentTimeMillis();
} else if (lastPosition.squaredDistanceTo(pos) > MOVE_EPSILON_SQUARED) {
lastMoveTime = System.currentTimeMillis();
lastPosition = pos;
}
}
while (STOP_KEY.wasPressed()) {
if (botEnabled) {
ChatMenu.send("§cBot stopped via hotkey.§r");
stopBot();
}
}
tickCount++;
if (botEnabled && tickCount % HEALTHCHECK_INTERVAL_TICKS == 0 && !activeFarmer.healthcheck()) {
ChatMenu.send("§cHealthcheck failed, stopping bot.§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();
});
}
}
@@ -0,0 +1,69 @@
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.NetherwartFarmer;
import skybot.farmers.PotatoFarmer;
import skybot.farmers.TestFarmer;
import skybot.farmers.WheatFarmer;
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("carrot").executes(context -> {
if (SkybotClient.startBot(new CarrotFarmer())) {
ChatMenu.send("§aStarting Carrot Farmer...§r");
}
return 1;
}))
.then(literal("netherwart").executes(context -> {
if (SkybotClient.startBot(new NetherwartFarmer())) {
ChatMenu.send("§aStarting Netherwart Farmer...§r");
}
return 1;
}))
.then(literal("wheat").executes(context -> {
if (SkybotClient.startBot(new WheatFarmer())) {
ChatMenu.send("§aStarting Wheat Farmer...§r");
}
return 1;
}))
.then(literal("potato").executes(context -> {
if (SkybotClient.startBot(new PotatoFarmer())) {
ChatMenu.send("§aStarting Potato Farmer...§r");
}
return 1;
})));
dispatcher.register(literal("skybot")
.then(literal("status").executes(context -> {
ChatMenu.send("Status: " + SkybotClient.botStatus());
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);
}
}
+150 -50
View File
@@ -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
@@ -20,9 +23,17 @@ public class Controller {
private final static KeyOverride sprinting = new KeyOverride();
private final static KeyOverride sneaking = new KeyOverride();
private static Double targetYaw = null;
private static Double targetPitch = null;
private static double turnSpeed = 10.0; // max degrees per tick
private final static KeyOverride attack = new KeyOverride(); // Left click
private final static KeyOverride use = new KeyOverride(); // Right click
// 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<InputUtil.Key, KeyOverride> 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);
@@ -52,21 +63,56 @@ public class Controller {
sneaking.set(pressed);
}
public static void lookAt(double yaw, double pitch) {
targetYaw = yaw;
targetPitch = MathHelper.clamp(pitch, -90.0, 90.0);
public static void setLeftClick(Boolean pressed) {
attack.set(pressed);
}
public static void setTurnSpeed(double degreesPerTick) {
turnSpeed = degreesPerTick;
public static void setRightClick(Boolean pressed) {
use.set(pressed);
}
public static void clearLook() {
targetYaw = null;
targetPitch = null;
/**
* 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);
}
/** Clears every override, handing all input back to the player. Call on disconnect. */
/**
* 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);
}
/**
* A setter bound to one physical key, e.g.
* new ClickButtonTask(Controller.key(InputUtil.GLFW_KEY_KP_7)).
*/
public static Consumer<Boolean> 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.
*/
public static void releaseAll() {
forward.set(null);
back.set(null);
@@ -75,43 +121,92 @@ public class Controller {
jumping.set(null);
sprinting.set(null);
sneaking.set(null);
clearLook();
attack.set(null);
use.set(null);
for (InputUtil.Key key : keyOverrides.keySet()) {
KeyBinding.setKeyPressed(key, false);
}
public static void tick(MinecraftClient client) {
ClientPlayerEntity player = client.player;
if (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);
if (targetYaw != null && targetPitch != null) {
applyLook(player);
}
}
private static void applyLook(ClientPlayerEntity player) {
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);
player.setYaw((float) (player.getYaw() + yawStep));
player.setPitch((float) MathHelper.clamp(player.getPitch() + pitchStep, -90.0, 90.0));
keyOverrides.clear();
// clearLook();
}
/**
* 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.
* Run this inside ClientTickEvents.END_CLIENT_TICK (20Hz).
* Handles keyboard state overrides aligned with game engine updates.
*/
public static void tick(MinecraftClient client) {
if (client.player == null)
return;
GameOptions options = client.options;
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::setPressed);
use.apply(options.useKey::setPressed);
for (Iterator<Map.Entry<InputUtil.Key, KeyOverride>> it = keyOverrides.entrySet().iterator(); it
.hasNext();) {
Map.Entry<InputUtil.Key, KeyOverride> entry = it.next();
InputUtil.Key key = entry.getKey();
KeyOverride override = entry.getValue();
override.apply(pressed -> KeyBinding.setKeyPressed(key, pressed));
if (override.isIdle()) {
it.remove();
}
}
}
/**
* 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();
// // 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.
*/
private static class KeyOverride {
private Boolean state;
@@ -121,14 +216,19 @@ public class Controller {
state = pressed;
}
void apply(KeyBinding binding) {
void apply(Consumer<Boolean> 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;
}
}
}
@@ -1,16 +0,0 @@
package skybot.controller.task;
public interface Task {
/** Called once, the tick this task becomes active. */
default void start() {
}
/** Called every tick while this task is active. */
void tick();
boolean isDone();
/** Called once, right after isDone() first returns true, before the next task starts. */
default void stop() {
}
}
@@ -0,0 +1,56 @@
package skybot.farmers;
import skybot.SkybotClient;
import skybot.controller.Controller;
import skybot.task.HoldKeyTask;
import skybot.task.PressButtonTask;
import skybot.task.ReleaseButtonTask;
import skybot.task.RunCommandTask;
import skybot.task.TaskQueue;
public class CarrotFarmer implements Farmer {
private static final long HEALTHCHECK_TIMEOUT_MILLIS = 4500;
private static final long MOVEMENT_TIMEOUT_MILLIS = 5000;
@Override
public boolean healthcheck() {
long now = System.currentTimeMillis();
boolean brokeRecently = now - SkybotClient.getLastBreakTime() < HEALTHCHECK_TIMEOUT_MILLIS;
boolean movedRecently = now - SkybotClient.getLastMoveTime() < MOVEMENT_TIMEOUT_MILLIS;
return brokeRecently && movedRecently;
}
@Override
public void enqueue(TaskQueue queue) {
queue.clear();
queue.add(new RunCommandTask("warp garden"), 1998);
int EPOCHS = 2;
for (int i = 0; i < EPOCHS; i++) {
queue.add(new PressButtonTask(Controller::setLeftClick), 2021);
queue.add(new HoldKeyTask(Controller::setLeft, 119555));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2015);
queue.add(new HoldKeyTask(Controller::setRight, 119402));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setLeft, 119223));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2203);
queue.add(new HoldKeyTask(Controller::setRight, 119198));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setLeft, 120155));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new RunCommandTask("warp garden"), 2126);
}
}
}
@@ -0,0 +1,12 @@
package skybot.farmers;
import skybot.task.TaskQueue;
public interface Farmer {
void enqueue(TaskQueue queue);
/** Periodic sanity check while this farmer is running. Returning false stops the bot. */
default boolean healthcheck() {
return true;
}
}
@@ -0,0 +1,56 @@
package skybot.farmers;
import skybot.SkybotClient;
import skybot.controller.Controller;
import skybot.task.HoldKeyTask;
import skybot.task.PressButtonTask;
import skybot.task.ReleaseButtonTask;
import skybot.task.RunCommandTask;
import skybot.task.TaskQueue;
public class NetherwartFarmer implements Farmer {
private static final long HEALTHCHECK_TIMEOUT_MILLIS = 4500;
private static final long MOVEMENT_TIMEOUT_MILLIS = 5000;
@Override
public boolean healthcheck() {
long now = System.currentTimeMillis();
boolean brokeRecently = now - SkybotClient.getLastBreakTime() < HEALTHCHECK_TIMEOUT_MILLIS;
boolean movedRecently = now - SkybotClient.getLastMoveTime() < MOVEMENT_TIMEOUT_MILLIS;
return brokeRecently && movedRecently;
}
@Override
public void enqueue(TaskQueue queue) {
queue.clear();
queue.add(new RunCommandTask("warp garden"), 1998);
int EPOCHS = 2;
for (int i = 0; i < EPOCHS; i++) {
queue.add(new PressButtonTask(Controller::setLeftClick), 2021);
queue.add(new HoldKeyTask(Controller::setLeft, 119555));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2015);
queue.add(new HoldKeyTask(Controller::setRight, 119402));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setLeft, 119223));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2203);
queue.add(new HoldKeyTask(Controller::setRight, 119198));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setLeft, 120155));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new RunCommandTask("warp garden"), 2126);
}
}
}
@@ -0,0 +1,56 @@
package skybot.farmers;
import skybot.SkybotClient;
import skybot.controller.Controller;
import skybot.task.HoldKeyTask;
import skybot.task.PressButtonTask;
import skybot.task.ReleaseButtonTask;
import skybot.task.RunCommandTask;
import skybot.task.TaskQueue;
public class PotatoFarmer implements Farmer {
private static final long HEALTHCHECK_TIMEOUT_MILLIS = 4500;
private static final long MOVEMENT_TIMEOUT_MILLIS = 5000;
@Override
public boolean healthcheck() {
long now = System.currentTimeMillis();
boolean brokeRecently = now - SkybotClient.getLastBreakTime() < HEALTHCHECK_TIMEOUT_MILLIS;
boolean movedRecently = now - SkybotClient.getLastMoveTime() < MOVEMENT_TIMEOUT_MILLIS;
return brokeRecently && movedRecently;
}
@Override
public void enqueue(TaskQueue queue) {
queue.clear();
queue.add(new RunCommandTask("warp garden"), 1998);
int EPOCHS = 2;
for (int i = 0; i < EPOCHS; i++) {
queue.add(new PressButtonTask(Controller::setLeftClick), 2021);
queue.add(new HoldKeyTask(Controller::setRight, 119555));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2015);
queue.add(new HoldKeyTask(Controller::setLeft, 119402));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setRight, 119223));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2203);
queue.add(new HoldKeyTask(Controller::setLeft, 119198));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setRight, 120155));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new RunCommandTask("warp garden"), 2126);
}
}
}
@@ -0,0 +1,18 @@
package skybot.farmers;
import net.minecraft.client.util.InputUtil;
import skybot.controller.Controller;
import skybot.task.ClickButtonTask;
import skybot.task.PauseTask;
import skybot.task.TaskQueue;
public class TestFarmer implements Farmer {
@Override
public void enqueue(TaskQueue queue) {
queue.clear();
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)));
}
}
@@ -0,0 +1,56 @@
package skybot.farmers;
import skybot.SkybotClient;
import skybot.controller.Controller;
import skybot.task.HoldKeyTask;
import skybot.task.PressButtonTask;
import skybot.task.ReleaseButtonTask;
import skybot.task.RunCommandTask;
import skybot.task.TaskQueue;
public class WheatFarmer implements Farmer {
private static final long HEALTHCHECK_TIMEOUT_MILLIS = 4500;
private static final long MOVEMENT_TIMEOUT_MILLIS = 5000;
@Override
public boolean healthcheck() {
long now = System.currentTimeMillis();
boolean brokeRecently = now - SkybotClient.getLastBreakTime() < HEALTHCHECK_TIMEOUT_MILLIS;
boolean movedRecently = now - SkybotClient.getLastMoveTime() < MOVEMENT_TIMEOUT_MILLIS;
return brokeRecently && movedRecently;
}
@Override
public void enqueue(TaskQueue queue) {
queue.clear();
queue.add(new RunCommandTask("warp garden"), 1998);
int EPOCHS = 2;
for (int i = 0; i < EPOCHS; i++) {
queue.add(new PressButtonTask(Controller::setLeftClick), 2021);
queue.add(new HoldKeyTask(Controller::setRight, 119555));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2015);
queue.add(new HoldKeyTask(Controller::setLeft, 119402));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setRight, 119223));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 2203);
queue.add(new HoldKeyTask(Controller::setLeft, 119198));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new PressButtonTask(Controller::setLeftClick), 1998);
queue.add(new HoldKeyTask(Controller::setRight, 120155));
queue.add(new ReleaseButtonTask(Controller::setLeftClick));
queue.add(new RunCommandTask("warp garden"), 2126);
}
}
}
@@ -0,0 +1,54 @@
package skybot.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<Boolean> setter;
private long endTime;
public ClickButtonTask(Consumer<Boolean> 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;
}
}
@@ -1,4 +1,4 @@
package skybot.controller.task;
package skybot.task;
import java.util.function.Consumer;
@@ -1,4 +1,4 @@
package skybot.controller.task;
package skybot.task;
/**
* Does nothing for a fixed duration; leaves whatever keys were already released
@@ -0,0 +1,31 @@
package skybot.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.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;
}
}
@@ -0,0 +1,31 @@
package skybot.task;
import net.minecraft.client.MinecraftClient;
/** Runs a Minecraft command (e.g. "/help"), the same as typing it in chat. */
public class RunCommandTask implements Task {
private final String command;
private boolean ran = false;
public RunCommandTask(String command) {
this.command = command.startsWith("/") ? command.substring(1) : command;
}
@Override
public void start() {
MinecraftClient client = MinecraftClient.getInstance();
if (client.player != null) {
client.player.networkHandler.sendChatCommand(command);
}
ran = true;
}
@Override
public void tick() {
}
@Override
public boolean isDone() {
return ran;
}
}
+24
View File
@@ -0,0 +1,24 @@
package skybot.task;
public interface Task {
/** Called once, the tick this task becomes active. */
default void start() {
}
/** Called every tick while this task is active. */
void tick();
boolean isDone();
/**
* 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();
}
}
@@ -1,4 +1,4 @@
package skybot.controller.task;
package skybot.task;
import java.util.ArrayDeque;
import java.util.Deque;
@@ -19,14 +19,27 @@ 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;
}
public TaskQueue add(Task task, long delayMs) {
pending.add(new PauseTask(delayMs));
pending.add(task);
return this;
}
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;
}
@@ -34,8 +47,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,24 +59,41 @@ 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 %s... (delayUntil: %dms)", current.name(), totalDelta));
current.start();
currentStarted = true;
taskStartTime = System.currentTimeMillis();
}
current.tick();
}
}
public boolean isDone() {
public boolean isEmpty() {
return current == null && pending.isEmpty();
}
public void clear() {
pending.clear();
current = null;
currentStarted = false;
delayUntil = 0;
rolledDelay = 0;
taskStartTime = 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,35 @@
package skybot.utilities;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import java.util.concurrent.ConcurrentLinkedQueue;
public class TickScheduler {
private record Task(Runnable action, int ticksRemaining) {
}
private static final ConcurrentLinkedQueue<Task> tasks = new ConcurrentLinkedQueue<>();
public static void register() {
ClientTickEvents.END_CLIENT_TICK.register(client -> tick());
}
public static void schedule(Runnable action, int delayTicks) {
tasks.add(new Task(action, delayTicks));
}
private static void tick() {
int size = tasks.size();
for (int i = 0; i < size; i++) {
Task t = tasks.poll();
if (t == null)
break;
int remaining = t.ticksRemaining() - 1;
if (remaining <= 0) {
t.action().run();
} else {
tasks.add(new Task(t.action(), remaining));
}
}
}
}