package xyz.nodrop.farmingtools.client.pathfinding; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.util.math.BlockPos; import xyz.nodrop.farmingtools.client.pathfinding.PathInput; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Consumer; /** * Central controller for pathfinding. * * ┌─────────────────────────────────────────────────────┐ * │ External script / command calls: │ * │ PathfindingController.INSTANCE.goTo(x, y, z) │ * │ PathfindingController.INSTANCE.stop() │ * │ PathfindingController.INSTANCE.isRunning() │ * └─────────────────────────────────────────────────────┘ * * The A* search runs on a background thread so the client never freezes. * Once the path is ready it is handed off to PathExecutor which ticks * on the main client thread. */ public class PathfindingController { public static final PathfindingController INSTANCE = new PathfindingController(); private final ExecutorService threadPool = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "farmingtools-pathfinder"); t.setDaemon(true); return t; }); private final PathExecutor executor = new PathExecutor(); /** Last status message — shown on HUD. */ private volatile String status = "Idle"; /** Optional callback fired when path is found / fails / finishes. */ private volatile Consumer onResult = null; private PathfindingController() {} // ── Registration ────────────────────────────────────────────────────────── public void register() { ClientTickEvents.END_CLIENT_TICK.register(client -> { executor.tick(client); // Auto-clear status when done if (!executor.isActive() && status.equals("Walking...")) { status = "Arrived"; } }); } // ── Public API (call from any thread) ───────────────────────────────────── /** * Start pathfinding to the given block position. * Computation happens on a background thread; walking starts automatically. * * @param onResult optional callback called with the result (may be null) */ public void goTo(int x, int y, int z, boolean flying, Consumer onResult) { this.onResult = onResult; MinecraftClient client = MinecraftClient.getInstance(); ClientPlayerEntity player = client.player; if (player == null || client.world == null) { status = "Not in world"; return; } BlockPos start = player.getBlockPos(); BlockPos goal = new BlockPos(x, y, z); status = "Searching..."; executor.stop(client); executor.setFlying(flying); threadPool.submit(() -> { List path = AStarPathfinder.find(client.world, start, goal, flying); if (path.isEmpty()) { status = "No path found"; if (onResult != null) onResult.accept(PathResult.failure(goal)); } else { status = "Walking..."; executor.start(path); if (onResult != null) onResult.accept(PathResult.success(goal, path.size())); } }); } /** Convenience overload without callback. */ public void goTo(int x, int y, int z) { goTo(x, y, z, false, null); } /** Convenience overload accepting BlockPos. */ public void goTo(BlockPos goal) { goTo(goal.getX(), goal.getY(), goal.getZ(), false, null); } /** Stop walking immediately. */ public void stop() { MinecraftClient client = MinecraftClient.getInstance(); executor.stop(client); status = "Stopped"; } public boolean isRunning() { return executor.isActive(); } public String getStatus() { return status; } public PathInput getCurrentInput() { return executor.getCurrentInput(); } public int getProgress() { return executor.getWaypointIndex(); } public int getPathLength() { return executor.getPathLength(); } public BlockPos getGoal() { return executor.getGoal(); } public List getCurrentPath() { return executor.getPath(); } public int getCurrentWaypointIndex() { return executor.getWaypointIndex(); } // W PathfindingController: public PathExecutor getExecutor() { return executor; } // ── Result record ───────────────────────────────────────────────────────── public record PathResult(boolean success, BlockPos goal, int length) { static PathResult success(BlockPos g, int len) { return new PathResult(true, g, len); } static PathResult failure(BlockPos g) { return new PathResult(false, g, 0); } } }