From 176395b9cdfa8cd50c9c85e83894933e3524e4b3 Mon Sep 17 00:00:00 2001 From: mmichlol Date: Mon, 1 Jun 2026 09:00:25 +0000 Subject: [PATCH] Upload files to "/" --- AStarPathfinder.java | 282 +++++++++++++++++++++++++++++++++++++ PathExecutor.java | 24 ++-- PathInput.java | 12 ++ PathNode.java | 35 +++++ PathfindingController.java | 135 ++++++++++++++++++ 5 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 AStarPathfinder.java create mode 100644 PathInput.java create mode 100644 PathNode.java create mode 100644 PathfindingController.java diff --git a/AStarPathfinder.java b/AStarPathfinder.java new file mode 100644 index 0000000..e503df6 --- /dev/null +++ b/AStarPathfinder.java @@ -0,0 +1,282 @@ +package xyz.nodrop.farmingtools.client.pathfinding; + +import net.minecraft.block.BlockState; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.block.*; +import net.minecraft.registry.tag.BlockTags; + +import java.util.*; + +public class AStarPathfinder { + + private static final int MAX_NODES = 100_000; + private static final int MAX_FALL = 3; + + private static final int[][] DIRS = { + {1,0}, {-1,0}, {0,1}, {0,-1}, + {1,1}, {1,-1}, {-1,1}, {-1,-1} + }; + private static final double SQRT2 = 1.41421356; + + public static List find(ClientWorld world, BlockPos start, BlockPos goal, boolean flying) { + PriorityQueue open = new PriorityQueue<>(); + Map gScore = new HashMap<>(); + + open.add(new PathNode(start, null, 0, heuristic(start, goal))); + gScore.put(start, 0.0); + + int expanded = 0; + while (!open.isEmpty() && expanded < MAX_NODES) { + PathNode current = open.poll(); + expanded++; + + if (current.pos.equals(goal)) { + List raw = reconstruct(current); + return smooth(world, raw, flying); + } + + for (PathNode nb : neighbours(world, current, goal, flying)) { + double existing = gScore.getOrDefault(nb.pos, Double.MAX_VALUE); + if (nb.g < existing) { + gScore.put(nb.pos, nb.g); + open.add(nb); + } + } + } + return Collections.emptyList(); + } + + private static List smooth(ClientWorld world, List raw, boolean flying) { + if (raw.size() <= 2) return raw; + + if (flying) return smoothFly(world, raw); + else return smoothWalk(world, raw); + } + + // ── Smoothing dla chodzenia (stary kod, tylko przemianowany) ────────────── + private static List smoothWalk(ClientWorld world, List raw) { + List smoothed = new ArrayList<>(); + smoothed.add(raw.get(0)); + + int anchor = 0; + int i = 2; + + while (i < raw.size()) { + BlockPos from = raw.get(anchor); + BlockPos to = raw.get(i); + boolean yChanged = raw.get(i).getY() != raw.get(i - 1).getY(); + + if (yChanged || !hasLineOfWalk(world, from, to)) { + smoothed.add(raw.get(i - 1)); + anchor = i - 1; + } + i++; + } + + smoothed.add(raw.get(raw.size() - 1)); + return smoothed; + } + + // ── Smoothing dla lotu — ray cast 3D ───────────────────────────────────── + private static List smoothFly(ClientWorld world, List raw) { + List smoothed = new ArrayList<>(); + smoothed.add(raw.get(0)); + + int anchor = 0; + int i = 2; + + while (i < raw.size()) { + BlockPos from = raw.get(anchor); + BlockPos to = raw.get(i); + + if (!hasLineOfFlight(world, from, to)) { + smoothed.add(raw.get(i - 1)); + anchor = i - 1; + } + i++; + } + + smoothed.add(raw.get(raw.size() - 1)); + return smoothed; + } + + // ── Ray cast 3D dla lotu (Bresenham 3D) ────────────────────────────────── + private static boolean hasLineOfFlight(ClientWorld world, BlockPos from, BlockPos to) { + int x0 = from.getX(), y0 = from.getY(), z0 = from.getZ(); + int x1 = to.getX(), y1 = to.getY(), z1 = to.getZ(); + + int dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0), dz = Math.abs(z1 - z0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int sz = z0 < z1 ? 1 : -1; + + int cx = x0, cy = y0, cz = z0; + + // Błędy dla Bresenhama 3D + int errXY = dx - dy; + int errXZ = dx - dz; + + int steps = Math.max(dx, Math.max(dy, dz)); + + for (int i = 0; i < steps; i++) { + BlockPos pos = new BlockPos(cx, cy, cz); + // Sprawdź czy gracz (2 bloki wysokości) przejdzie + if (!isPassable(world, pos) || !isPassable(world, pos.up())) return false; + + int e2XY = 2 * errXY; + int e2XZ = 2 * errXZ; + + if (e2XY > -dy) { errXY -= dy; cx += sx; } + if (e2XY < dx) { errXY += dx; cy += sy; } + if (e2XZ > -dz) { errXZ -= dz; } + if (e2XZ < dx) { errXZ += dx; cz += sz; } + } + return true; + } + + private static boolean hasLineOfWalk(ClientWorld world, BlockPos from, BlockPos to) { + if (from.getY() != to.getY()) return false; + + int x0 = from.getX(), z0 = from.getZ(); + int x1 = to.getX(), z1 = to.getZ(); + int y = from.getY(); + + int dx = Math.abs(x1 - x0), dz = Math.abs(z1 - z0); + int sx = x0 < x1 ? 1 : -1, sz = z0 < z1 ? 1 : -1; + int err = dx - dz; + + int cx = x0, cz = z0; + while (true) { + if (cx == x1 && cz == z1) break; + + BlockPos pos = new BlockPos(cx, y, cz); + if (!canPass(world, pos) || !isSolid(world, pos.down())) return false; + + int e2 = 2 * err; + if (e2 > -dz) { err -= dz; cx += sx; } + if (e2 < dx) { err += dx; cz += sz; } + } + return true; + } + + private static List neighbours(ClientWorld world, PathNode current, BlockPos goal, boolean flying) { + List result = new ArrayList<>(); + BlockPos pos = current.pos; + + if (flying) { + // ── Tryb lotu — ruch we wszystkich 26 kierunkach ────────────────── + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dy == 0 && dz == 0) continue; + BlockPos nb = pos.add(dx, dy, dz); + if (isPassable(world, nb) && isPassable(world, nb.up())) { + double cost = Math.sqrt(dx*dx + dy*dy + dz*dz); + add(result, nb, current, goal, cost); + } + } + } + } + return result; + } + + for (int[] d : DIRS) { + int dx = d[0], dz = d[1]; + boolean diagonal = (dx != 0 && dz != 0); + double baseCost = diagonal ? SQRT2 : 1.0; + + int nx = pos.getX() + dx; + int nz = pos.getZ() + dz; + + if (diagonal) { + if (!canPass(world, new BlockPos(pos.getX() + dx, pos.getY(), pos.getZ()))) continue; + if (!canPass(world, new BlockPos(pos.getX(), pos.getY(), pos.getZ() + dz))) continue; + } + + BlockPos flat = new BlockPos(nx, pos.getY(), nz); + if (canStand(world, flat)) add(result, flat, current, goal, baseCost); + + BlockPos up = new BlockPos(nx, pos.getY() + 1, nz); + if (canStand(world, up) && isPassable(world, pos.up())) + add(result, up, current, goal, baseCost + 0.5); + + for (int drop = 1; drop <= MAX_FALL; drop++) { + BlockPos fell = new BlockPos(nx, pos.getY() - drop, nz); + if (canStand(world, fell)) { + add(result, fell, current, goal, baseCost + drop * 0.5); + break; + } + if (!isPassable(world, new BlockPos(nx, pos.getY() - drop, nz))) break; + } + } + + for (int drop = 1; drop <= MAX_FALL; drop++) { + BlockPos fell = pos.down(drop); + if (canStand(world, fell)) { + add(result, fell, current, goal, drop * 0.5); + break; + } + if (!isPassable(world, fell)) break; + } + + return result; + } + + private static void add(List list, BlockPos pos, + PathNode parent, BlockPos goal, double cost) { + list.add(new PathNode(pos, parent, parent.g + cost, heuristic(pos, goal))); + } + + private static boolean canStand(ClientWorld world, BlockPos feet) { + return isSolid(world, feet.down()) + && isPassable(world, feet) + && isPassable(world, feet.up()); + } + + public static boolean canPass(ClientWorld world, BlockPos pos) { + return isPassable(world, pos) && isPassable(world, pos.up()); + } + + private static final Set BLOCKED_BLOCKS = Set.of( + Blocks.COBWEB, + Blocks.SWEET_BERRY_BUSH, + Blocks.BAMBOO, + Blocks.BAMBOO_SAPLING, + Blocks.CACTUS, + Blocks.POWDER_SNOW + ); + + private static boolean isPassable(ClientWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + Block block = state.getBlock(); + + // Block blocks from tags + if (state.isIn(BlockTags.LEAVES)) return false; + if (state.isIn(BlockTags.FENCES)) return false; + if (state.isIn(BlockTags.WALLS)) return false; + + // Block blocks from list + if (BLOCKED_BLOCKS.contains(block)) return false; + + return !state.isSolidBlock(world, pos); + } + + private static boolean isSolid(ClientWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + return state.isSolidBlock(world, pos); + } + + private static double heuristic(BlockPos a, BlockPos b) { + double dx = a.getX() - b.getX(); + double dy = a.getY() - b.getY(); + double dz = a.getZ() - b.getZ(); + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + private static List reconstruct(PathNode node) { + LinkedList path = new LinkedList<>(); + for (PathNode n = node; n != null; n = n.parent) path.addFirst(n.pos); + return path; + } +} \ No newline at end of file diff --git a/PathExecutor.java b/PathExecutor.java index 4957bd3..9d6c168 100644 --- a/PathExecutor.java +++ b/PathExecutor.java @@ -230,20 +230,20 @@ public class PathExecutor { // ── Oblicz mnożnik prędkości ────────────────────────────────────────── float speedMult = 1.0f; - // 1. Hamowanie przed celem (ostatnie N bloków) + // 1. Hamowanie przed celem (ostatnie N bloków)z boolean isLastWaypoint = (waypointIndex == path.size() - 1); - if (flying) { - double distToTarget = playerPos.distanceTo(targetPos); - if (isLastWaypoint) { + // if (flying) { + //double distToTarget = playerPos.distanceTo(targetPos); + // if (isLastWaypoint) { // Hamuj płynnie na ostatnich 6 blokach - speedMult *= (float) Math.min(1.0, distToTarget / 6.0); - } - } else { - double distToTarget = horizontalDist(playerPos, targetPos); - if (isLastWaypoint) { - speedMult *= (float) Math.min(1.0, distToTarget / 4.0); - } - } + //speedMult *= (float) Math.min(1.0, distToTarget / 0.1); + // } + //} else { + //double distToTarget = horizontalDist(playerPos, targetPos); + //if (isLastWaypoint) { + //speedMult *= (float) Math.min(1.0, distToTarget / 4.0); + //} + //} // 2. Hamowanie przed zakrętem — patrz na kąt do NASTĘPNEGO waypointu if (waypointIndex + 1 < path.size()) { diff --git a/PathInput.java b/PathInput.java new file mode 100644 index 0000000..9397127 --- /dev/null +++ b/PathInput.java @@ -0,0 +1,12 @@ +package xyz.nodrop.farmingtools.client.pathfinding; + +/** + * Snapshot of movement input for one tick. + * Passed from PathExecutor → MixinKeyboardInput. + * + * movementForward: 1.0 = forward, -1.0 = back + * movementSideways: 1.0 = left, -1.0 = right (MC convention) + */ +public record PathInput(float forward, float sideways, float upward, boolean jumping, boolean sprint, boolean sneaking) { + public static final PathInput NONE = new PathInput(0, 0, 0, false, false, false); +} diff --git a/PathNode.java b/PathNode.java new file mode 100644 index 0000000..1ac2966 --- /dev/null +++ b/PathNode.java @@ -0,0 +1,35 @@ +package xyz.nodrop.farmingtools.client.pathfinding; + +import net.minecraft.util.math.BlockPos; + +/** + * Single node in the A* search graph. + * Comparable by f = g + h so it can be used directly in a PriorityQueue. + */ +public class PathNode implements Comparable { + + public final BlockPos pos; + public final PathNode parent; + + /** Cost from start to this node. */ + public final double g; + + /** Heuristic cost estimate from this node to goal. */ + public final double h; + + public PathNode(BlockPos pos, PathNode parent, double g, double h) { + this.pos = pos; + this.parent = parent; + this.g = g; + this.h = h; + } + + public double f() { + return g + h; + } + + @Override + public int compareTo(PathNode other) { + return Double.compare(this.f(), other.f()); + } +} diff --git a/PathfindingController.java b/PathfindingController.java new file mode 100644 index 0000000..570f962 --- /dev/null +++ b/PathfindingController.java @@ -0,0 +1,135 @@ +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); } + } +} \ No newline at end of file