@@ -1,14 +1,41 @@
|
||||
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.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.minecraft.client.network.ServerInfo;
|
||||
import skybot.controller.Controller;
|
||||
import skybot.controller.task.HoldKeyTask;
|
||||
import skybot.controller.task.PauseTask;
|
||||
import skybot.controller.task.TaskQueue;
|
||||
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();
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
TASK_QUEUE.tick();
|
||||
Controller.tick(client);
|
||||
});
|
||||
|
||||
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)
|
||||
@@ -21,6 +48,9 @@ public class SkybotClient implements ClientModInitializer {
|
||||
});
|
||||
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
|
||||
TASK_QUEUE.clear();
|
||||
Controller.releaseAll();
|
||||
|
||||
ServerInfo server = client.getCurrentServerEntry();
|
||||
if (server == null || !server.address.contains(HYPIXEL_URL))
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package skybot.controller;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Owns simulated input state (movement keys + look direction) and applies it
|
||||
* to the client each tick. Higher-level behavior code should set state here
|
||||
* rather than touching GameOptions/player rotation directly.
|
||||
*/
|
||||
public class Controller {
|
||||
private final static KeyOverride forward = new KeyOverride();
|
||||
private final static KeyOverride back = new KeyOverride();
|
||||
private final static KeyOverride left = new KeyOverride();
|
||||
private final static KeyOverride right = new KeyOverride();
|
||||
private final static KeyOverride jumping = new KeyOverride();
|
||||
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
|
||||
|
||||
public static void setForward(Boolean pressed) {
|
||||
forward.set(pressed);
|
||||
}
|
||||
|
||||
public static void setBack(Boolean pressed) {
|
||||
back.set(pressed);
|
||||
}
|
||||
|
||||
public static void setLeft(Boolean pressed) {
|
||||
left.set(pressed);
|
||||
}
|
||||
|
||||
public static void setRight(Boolean pressed) {
|
||||
right.set(pressed);
|
||||
}
|
||||
|
||||
public static void setJumping(Boolean pressed) {
|
||||
jumping.set(pressed);
|
||||
}
|
||||
|
||||
public static void setSprinting(Boolean pressed) {
|
||||
sprinting.set(pressed);
|
||||
}
|
||||
|
||||
public static void setSneaking(Boolean pressed) {
|
||||
sneaking.set(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);
|
||||
left.set(null);
|
||||
right.set(null);
|
||||
jumping.set(null);
|
||||
sprinting.set(null);
|
||||
sneaking.set(null);
|
||||
clearLook();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private boolean controlled = false;
|
||||
|
||||
void set(Boolean pressed) {
|
||||
state = pressed;
|
||||
}
|
||||
|
||||
void apply(KeyBinding binding) {
|
||||
if (state != null) {
|
||||
binding.setPressed(state);
|
||||
controlled = true;
|
||||
} else if (controlled) {
|
||||
binding.setPressed(false);
|
||||
controlled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package skybot.controller.task;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Holds one Controller key override for a fixed duration, then releases it. */
|
||||
public class HoldKeyTask implements Task {
|
||||
private final Consumer<Boolean> setter;
|
||||
private final long durationMillis;
|
||||
private long endTime;
|
||||
|
||||
public HoldKeyTask(Consumer<Boolean> setter, long durationMillis) {
|
||||
this.setter = setter;
|
||||
this.durationMillis = durationMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
endTime = System.currentTimeMillis() + durationMillis;
|
||||
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(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package skybot.controller.task;
|
||||
|
||||
/**
|
||||
* Does nothing for a fixed duration; leaves whatever keys were already released
|
||||
* alone.
|
||||
*/
|
||||
public class PauseTask implements Task {
|
||||
private final long durationMillis;
|
||||
private long endTime;
|
||||
|
||||
public PauseTask(long durationMillis) {
|
||||
this.durationMillis = durationMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
endTime = System.currentTimeMillis() + durationMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
if (endTime == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return System.currentTimeMillis() >= endTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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,70 @@
|
||||
package skybot.controller.task;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import skybot.utilities.ChatMenu;
|
||||
|
||||
/**
|
||||
* Runs Tasks one at a time, in order, advancing once the active task reports
|
||||
* done.
|
||||
*/
|
||||
public class TaskQueue {
|
||||
private final Deque<Task> pending = new ArrayDeque<>();
|
||||
private Task current;
|
||||
private boolean currentStarted = false;
|
||||
|
||||
private long delayUntil = 0;
|
||||
|
||||
// Track the exact random delay we rolled for the active task
|
||||
private long rolledDelay = 0;
|
||||
|
||||
public TaskQueue add(Task task) {
|
||||
pending.add(task);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (current != null && current.isDone()) {
|
||||
current.stop();
|
||||
current = null;
|
||||
currentStarted = false;
|
||||
}
|
||||
|
||||
if (current == null) {
|
||||
current = pending.poll();
|
||||
if (current != null) {
|
||||
// Roll the random delay
|
||||
rolledDelay = ThreadLocalRandom.current().nextLong(51, 200);
|
||||
delayUntil = System.currentTimeMillis() + rolledDelay;
|
||||
}
|
||||
}
|
||||
|
||||
if (current != null && System.currentTimeMillis() >= delayUntil) {
|
||||
if (!currentStarted) {
|
||||
// Calculate the actual elapsed overhead due to Minecraft's 50ms tick rate
|
||||
long actualOverhead = System.currentTimeMillis() - delayUntil;
|
||||
long totalDelta = rolledDelay + actualOverhead;
|
||||
|
||||
ChatMenu.send(
|
||||
String.format("Starting new job... (Target delay: %dms, Actual delta: %dms)", rolledDelay, totalDelta));
|
||||
|
||||
current.start();
|
||||
currentStarted = true;
|
||||
}
|
||||
|
||||
current.tick();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return current == null && pending.isEmpty();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
pending.clear();
|
||||
current = null;
|
||||
delayUntil = 0;
|
||||
rolledDelay = 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user