Upload files to "/"
This commit is contained in:
282
AStarPathfinder.java
Normal file
282
AStarPathfinder.java
Normal file
@@ -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<BlockPos> find(ClientWorld world, BlockPos start, BlockPos goal, boolean flying) {
|
||||
PriorityQueue<PathNode> open = new PriorityQueue<>();
|
||||
Map<BlockPos, Double> 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<BlockPos> 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<BlockPos> smooth(ClientWorld world, List<BlockPos> 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<BlockPos> smoothWalk(ClientWorld world, List<BlockPos> raw) {
|
||||
List<BlockPos> 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<BlockPos> smoothFly(ClientWorld world, List<BlockPos> raw) {
|
||||
List<BlockPos> 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<PathNode> neighbours(ClientWorld world, PathNode current, BlockPos goal, boolean flying) {
|
||||
List<PathNode> 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<PathNode> 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<Block> 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<BlockPos> reconstruct(PathNode node) {
|
||||
LinkedList<BlockPos> path = new LinkedList<>();
|
||||
for (PathNode n = node; n != null; n = n.parent) path.addFirst(n.pos);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
12
PathInput.java
Normal file
12
PathInput.java
Normal file
@@ -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);
|
||||
}
|
||||
35
PathNode.java
Normal file
35
PathNode.java
Normal file
@@ -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<PathNode> {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
135
PathfindingController.java
Normal file
135
PathfindingController.java
Normal file
@@ -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<PathResult> 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<PathResult> 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<BlockPos> 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<BlockPos> 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); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user