Upload files to "/"

This commit is contained in:
2026-06-01 09:00:25 +00:00
parent 30b3ef72a4
commit 176395b9cd
5 changed files with 476 additions and 12 deletions

282
AStarPathfinder.java Normal file
View 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;
}
}

View File

@@ -230,20 +230,20 @@ public class PathExecutor {
// ── Oblicz mnożnik prędkości ────────────────────────────────────────── // ── Oblicz mnożnik prędkości ──────────────────────────────────────────
float speedMult = 1.0f; 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); boolean isLastWaypoint = (waypointIndex == path.size() - 1);
if (flying) { // if (flying) {
double distToTarget = playerPos.distanceTo(targetPos); //double distToTarget = playerPos.distanceTo(targetPos);
if (isLastWaypoint) { // if (isLastWaypoint) {
// Hamuj płynnie na ostatnich 6 blokach // Hamuj płynnie na ostatnich 6 blokach
speedMult *= (float) Math.min(1.0, distToTarget / 6.0); //speedMult *= (float) Math.min(1.0, distToTarget / 0.1);
} // }
} else { //} else {
double distToTarget = horizontalDist(playerPos, targetPos); //double distToTarget = horizontalDist(playerPos, targetPos);
if (isLastWaypoint) { //if (isLastWaypoint) {
speedMult *= (float) Math.min(1.0, distToTarget / 4.0); //speedMult *= (float) Math.min(1.0, distToTarget / 4.0);
} //}
} //}
// 2. Hamowanie przed zakrętem — patrz na kąt do NASTĘPNEGO waypointu // 2. Hamowanie przed zakrętem — patrz na kąt do NASTĘPNEGO waypointu
if (waypointIndex + 1 < path.size()) { if (waypointIndex + 1 < path.size()) {

12
PathInput.java Normal file
View 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
View 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
View 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); }
}
}