216 行
6.5 KiB
Java
216 行
6.5 KiB
Java
package com.autorail.pathing;
|
|
|
|
import net.minecraft.block.BlockState;
|
|
import net.minecraft.block.Blocks;
|
|
import net.minecraft.client.world.ClientWorld;
|
|
import net.minecraft.registry.tag.FluidTags;
|
|
import net.minecraft.util.math.BlockPos;
|
|
import net.minecraft.util.math.Direction;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.PriorityQueue;
|
|
|
|
/**
|
|
* 自动走到远处的起始点:内置 A* 步行寻路(4 向 + 跳 1 格 + 落 3 格 + 可涉水)。
|
|
*
|
|
* 与铺轨寻路解耦:先走过去,再从起点开始铺轨;走不到则明确报错让用户手动接近。
|
|
*/
|
|
public class WalkPathfinder {
|
|
|
|
public enum State { RUNNING, FOUND, FAILED }
|
|
|
|
private static final int MARGIN = 32;
|
|
private static final long MAX_EXPANSIONS = 200_000L;
|
|
private static final float COST_JUMP = 1.7f;
|
|
private static final float COST_WATER = 0.8f;
|
|
|
|
private final ClientWorld world;
|
|
private final BlockPos start;
|
|
private final BlockPos goal;
|
|
|
|
private final PriorityQueue<Node> open = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f));
|
|
private final Map<Long, Node> best = new HashMap<>();
|
|
private final List<BlockPos> result = new ArrayList<>();
|
|
|
|
private State state = State.RUNNING;
|
|
private String failKey;
|
|
private long expansions;
|
|
private int opened;
|
|
|
|
private int minX, minY, minZ, maxX, maxY, maxZ;
|
|
|
|
public WalkPathfinder(ClientWorld world, BlockPos start, BlockPos goal) {
|
|
this.world = world;
|
|
this.start = start.toImmutable();
|
|
this.goal = goal.toImmutable();
|
|
computeRegion();
|
|
Node s = new Node(this.start, null, 0f, h(this.start));
|
|
best.put(this.start.asLong(), s);
|
|
open.add(s);
|
|
opened++;
|
|
}
|
|
|
|
public State getState() {
|
|
return state;
|
|
}
|
|
|
|
public String getFailKey() {
|
|
return failKey;
|
|
}
|
|
|
|
public List<BlockPos> getResult() {
|
|
return result;
|
|
}
|
|
|
|
public int getOpened() {
|
|
return opened;
|
|
}
|
|
|
|
/** 每帧分片执行,最多展开 budget 个节点,不卡客户端线程。 */
|
|
public void step(int budget) {
|
|
if (state != State.RUNNING) {
|
|
return;
|
|
}
|
|
for (int b = 0; b < budget; b++) {
|
|
Node cur = poll();
|
|
if (cur == null) {
|
|
fail("autorail.err.walkFail");
|
|
return;
|
|
}
|
|
if (cur.pos.equals(goal)) {
|
|
rebuild(cur);
|
|
state = State.FOUND;
|
|
return;
|
|
}
|
|
expand(cur);
|
|
if (++expansions > MAX_EXPANSIONS) {
|
|
fail("autorail.err.walkFail");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Node poll() {
|
|
while (true) {
|
|
Node n = open.poll();
|
|
if (n == null) {
|
|
return null;
|
|
}
|
|
if (best.get(n.pos.asLong()) == n) {
|
|
return n;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void expand(Node cur) {
|
|
for (Direction d : Direction.Type.HORIZONTAL) {
|
|
BlockPos base = cur.pos.offset(d);
|
|
// 跳上 1 格
|
|
if (passHead(cur.pos.up(2))) {
|
|
tryMove(cur, base.up(), COST_JUMP, true);
|
|
}
|
|
// 平走 / 涉水
|
|
tryMove(cur, base, 1.0f, false);
|
|
// 落下 1-3 格
|
|
for (int f = 1; f <= 3; f++) {
|
|
tryMove(cur, base.down(f), 1.0f + 0.25f * f, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void tryMove(Node cur, BlockPos target, float baseCost, boolean jump) {
|
|
if (!inRegion(target) || !walkable(target) || !passHead(target.up())) {
|
|
return;
|
|
}
|
|
BlockState sup = world.getBlockState(target.down());
|
|
boolean solid = sup.isSolidBlock(world, target.down());
|
|
boolean water = sup.getFluidState().isIn(FluidTags.WATER);
|
|
if (!solid && !water) {
|
|
return;
|
|
}
|
|
if (jump && !solid) {
|
|
return; // 跳跃必须落在实心方块上
|
|
}
|
|
float cost = baseCost;
|
|
if (world.getBlockState(target).getFluidState().isIn(FluidTags.WATER)) {
|
|
cost += COST_WATER;
|
|
}
|
|
float g = cur.g + cost;
|
|
long key = target.asLong();
|
|
Node old = best.get(key);
|
|
if (old == null || g < old.g - 1.0e-4f) {
|
|
Node n = new Node(target, cur, g, g + h(target));
|
|
best.put(key, n);
|
|
open.add(n);
|
|
opened++;
|
|
}
|
|
}
|
|
|
|
private void rebuild(Node goalNode) {
|
|
result.clear();
|
|
for (Node n = goalNode; n != null; n = n.parent) {
|
|
result.add(n.pos);
|
|
}
|
|
java.util.Collections.reverse(result);
|
|
}
|
|
|
|
private void computeRegion() {
|
|
minX = Math.min(start.getX(), goal.getX()) - MARGIN;
|
|
maxX = Math.max(start.getX(), goal.getX()) + MARGIN;
|
|
minZ = Math.min(start.getZ(), goal.getZ()) - MARGIN;
|
|
maxZ = Math.max(start.getZ(), goal.getZ()) + MARGIN;
|
|
minY = Math.max(world.getBottomY() + 1, Math.min(start.getY(), goal.getY()) - MARGIN);
|
|
maxY = Math.min(world.getTopYInclusive() - 2, Math.max(start.getY(), goal.getY()) + MARGIN);
|
|
}
|
|
|
|
private boolean inRegion(BlockPos p) {
|
|
return p.getX() >= minX && p.getX() <= maxX
|
|
&& p.getY() >= minY && p.getY() <= maxY
|
|
&& p.getZ() >= minZ && p.getZ() <= maxZ;
|
|
}
|
|
|
|
private float h(BlockPos p) {
|
|
return 1.0f * (Math.abs(p.getX() - goal.getX())
|
|
+ Math.abs(p.getY() - goal.getY())
|
|
+ Math.abs(p.getZ() - goal.getZ()));
|
|
}
|
|
|
|
private void fail(String key) {
|
|
failKey = key;
|
|
state = State.FAILED;
|
|
}
|
|
|
|
/** 站立格:空气 / 可替换 / 铁轨(无碰撞)/ 水。岩浆不可走。 */
|
|
private boolean walkable(BlockPos pos) {
|
|
BlockState s = world.getBlockState(pos);
|
|
return s.isAir()
|
|
|| (s.getFluidState().isEmpty() && s.isReplaceable())
|
|
|| s.isOf(Blocks.RAIL)
|
|
|| s.isOf(Blocks.POWERED_RAIL)
|
|
|| s.isOf(Blocks.DETECTOR_RAIL)
|
|
|| s.getFluidState().isIn(FluidTags.WATER);
|
|
}
|
|
|
|
private boolean passHead(BlockPos pos) {
|
|
return walkable(pos);
|
|
}
|
|
|
|
private static final class Node {
|
|
final BlockPos pos;
|
|
final Node parent;
|
|
final float g;
|
|
final float f;
|
|
|
|
Node(BlockPos pos, Node parent, float g, float f) {
|
|
this.pos = pos;
|
|
this.parent = parent;
|
|
this.g = g;
|
|
this.f = f;
|
|
}
|
|
}
|
|
}
|