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 open = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f)); private final Map best = new HashMap<>(); private final List 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 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; } } }