mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add Phase 1 core pathfinding architecture
Implements the new Baritone-inspired A* pathfinding system: - Core types: PathNode, PathResult, MoveResult, MoveType, ActionCosts - BinaryHeapOpenSet min-heap for A* open set - AStarPathFinder with timeout, cancellation, partial path support - CalculationContext for thread-safe world state snapshots - MoveHelper for block passability checks - IGoal interface + GoalBlock, GoalXZ, GoalNear, GoalComposite - IMove interface + MoveTraverse, MoveDiagonal, MoveAscend, MoveDescend, MoveClimb - /pathfind command for testing the new pathfinder Made-with: Cursor
This commit is contained in:
parent
3efe63c54d
commit
1abab20f17
23 changed files with 1222 additions and 0 deletions
133
MinecraftClient/Commands/Pathfind.cs
Normal file
133
MinecraftClient/Commands/Pathfind.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Pathfind : Command
|
||||
{
|
||||
public override string CmdName => "pathfind";
|
||||
public override string CmdUsage => "pathfind <x y z>";
|
||||
public override string CmdDesc => Translations.cmd_pathfind_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source)))
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Then(l => l.Argument("location", MccArguments.Location())
|
||||
.Executes(r => DoPathfind(r.Source, MccArguments.GetLocation(r, "location"))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r)
|
||||
{
|
||||
return r.SetAndReturn(GetCmdDescTranslated());
|
||||
}
|
||||
|
||||
private int DoPathfind(CmdResult r, Location goal)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetTerrainEnabled())
|
||||
return r.SetAndReturn(Status.FailNeedTerrain);
|
||||
|
||||
Location current = handler.GetCurrentLocation();
|
||||
goal.ToAbsolute(current);
|
||||
|
||||
int startX = (int)Math.Floor(current.X);
|
||||
int startY = (int)Math.Floor(current.Y);
|
||||
int startZ = (int)Math.Floor(current.Z);
|
||||
int goalX = (int)Math.Floor(goal.X);
|
||||
int goalY = (int)Math.Floor(goal.Y);
|
||||
int goalZ = (int)Math.Floor(goal.Z);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Planning from ({startX},{startY},{startZ}) to ({goalX},{goalY},{goalZ})");
|
||||
|
||||
var ctx = new CalculationContext(
|
||||
handler.GetWorld(),
|
||||
canSprint: true,
|
||||
maxFallHeight: 3);
|
||||
|
||||
var finder = new AStarPathFinder();
|
||||
finder.DebugLog = msg => handler.Log.Info(msg);
|
||||
|
||||
var goalObj = new GoalBlock(goalX, goalY, goalZ);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var result = finder.Calculate(ctx, startX, startY, startZ, goalObj, cts.Token, timeoutMs: 10000);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Result: {result.Status}, {result.Path.Count} nodes, " +
|
||||
$"{result.NodesExplored} explored, {result.ElapsedMs}ms");
|
||||
|
||||
if (result.Path.Count > 0)
|
||||
{
|
||||
handler.Log.Info("[Pathfind] Path waypoints:");
|
||||
for (int i = 0; i < result.Path.Count; i++)
|
||||
{
|
||||
var n = result.Path[i];
|
||||
handler.Log.Info($" [{i}] ({n.X},{n.Y},{n.Z}) via {n.MoveUsed}");
|
||||
}
|
||||
|
||||
handler.Log.Info("[Pathfind] Beginning movement along path...");
|
||||
FollowPath(handler, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.Log.Warn("[Pathfind] No path found!");
|
||||
}
|
||||
});
|
||||
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_pathfind_started, goalX, goalY, goalZ));
|
||||
}
|
||||
|
||||
private static void FollowPath(McClient handler, PathResult result)
|
||||
{
|
||||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
var target = new Location(node.X + 0.5, node.Y, node.Z + 0.5);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Moving to waypoint [{i}]: ({node.X},{node.Y},{node.Z}) via {node.MoveUsed}");
|
||||
|
||||
bool success = handler.MoveTo(target, allowUnsafe: true, allowDirectTeleport: false, timeout: TimeSpan.FromSeconds(10));
|
||||
if (!success)
|
||||
{
|
||||
handler.Log.Warn($"[Pathfind] Old pathfinder failed to plan sub-path to ({node.X},{node.Y},{node.Z}), trying direct teleport");
|
||||
handler.MoveTo(target, allowUnsafe: true, allowDirectTeleport: true);
|
||||
}
|
||||
|
||||
int maxWaitTicks = 200;
|
||||
int waited = 0;
|
||||
while (handler.ClientIsMoving() && waited < maxWaitTicks)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
waited++;
|
||||
}
|
||||
|
||||
var cur = handler.GetCurrentLocation();
|
||||
double dx = cur.X - target.X;
|
||||
double dz = cur.Z - target.Z;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
handler.Log.Info($"[Pathfind] Arrived near waypoint [{i}], pos=({cur.X:F2},{cur.Y:F2},{cur.Z:F2}), horizDist={Math.Sqrt(horizDistSq):F2}");
|
||||
}
|
||||
|
||||
handler.Log.Info("[Pathfind] Path execution complete!");
|
||||
}
|
||||
}
|
||||
}
|
||||
189
MinecraftClient/Pathing/Core/AStarPathFinder.cs
Normal file
189
MinecraftClient/Pathing/Core/AStarPathFinder.cs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using MinecraftClient.Pathing.Moves;
|
||||
using MinecraftClient.Pathing.Moves.Impl;
|
||||
|
||||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
public sealed class AStarPathFinder
|
||||
{
|
||||
private readonly IMove[] _allMoves;
|
||||
private readonly int _maxChunkBorderFetch;
|
||||
|
||||
public Action<string>? DebugLog { get; set; }
|
||||
|
||||
public AStarPathFinder(IMove[]? moves = null, int maxChunkBorderFetch = 64)
|
||||
{
|
||||
_allMoves = moves ?? BuildDefaultMoves();
|
||||
_maxChunkBorderFetch = maxChunkBorderFetch;
|
||||
}
|
||||
|
||||
public static IMove[] BuildDefaultMoves()
|
||||
{
|
||||
var moves = new List<IMove>();
|
||||
|
||||
int[] offsets = [1, -1];
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
moves.Add(new MoveTraverse(dx, 0));
|
||||
moves.Add(new MoveAscend(dx, 0));
|
||||
moves.Add(new MoveDescend(dx, 0));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
moves.Add(new MoveTraverse(0, dz));
|
||||
moves.Add(new MoveAscend(0, dz));
|
||||
moves.Add(new MoveDescend(0, dz));
|
||||
}
|
||||
|
||||
moves.Add(new MoveDiagonal(1, 1));
|
||||
moves.Add(new MoveDiagonal(1, -1));
|
||||
moves.Add(new MoveDiagonal(-1, 1));
|
||||
moves.Add(new MoveDiagonal(-1, -1));
|
||||
|
||||
moves.Add(new MoveClimb(true));
|
||||
moves.Add(new MoveClimb(false));
|
||||
|
||||
return [.. moves];
|
||||
}
|
||||
|
||||
public PathResult Calculate(
|
||||
CalculationContext ctx,
|
||||
int startX, int startY, int startZ,
|
||||
IGoal goal,
|
||||
CancellationToken ct,
|
||||
long timeoutMs = 5000)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var openSet = new BinaryHeapOpenSet(4096);
|
||||
var nodeMap = new Dictionary<long, PathNode>(4096);
|
||||
|
||||
var startNode = new PathNode(startX, startY, startZ)
|
||||
{
|
||||
GCost = 0,
|
||||
HCost = goal.Heuristic(startX, startY, startZ),
|
||||
IsOpen = true
|
||||
};
|
||||
openSet.Insert(startNode);
|
||||
nodeMap[startNode.PackedPosition] = startNode;
|
||||
|
||||
int nodesExplored = 0;
|
||||
int unloadedChunkHits = 0;
|
||||
PathNode? bestPartialNode = startNode;
|
||||
double bestPartialScore = startNode.HCost + startNode.GCost * 0.5;
|
||||
MoveResult moveResult = default;
|
||||
|
||||
DebugLog?.Invoke($"[A*] Start ({startX},{startY},{startZ}), goal={goal}");
|
||||
|
||||
while (openSet.Count > 0)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Cancelled after {nodesExplored} nodes, {sw.ElapsedMilliseconds}ms");
|
||||
break;
|
||||
}
|
||||
|
||||
if (sw.ElapsedMilliseconds > timeoutMs)
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Timeout ({timeoutMs}ms) after {nodesExplored} nodes");
|
||||
break;
|
||||
}
|
||||
|
||||
var current = openSet.RemoveMin();
|
||||
current.IsClosed = true;
|
||||
nodesExplored++;
|
||||
|
||||
if (goal.IsInGoal(current.X, current.Y, current.Z))
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Goal reached! {nodesExplored} nodes, {sw.ElapsedMilliseconds}ms");
|
||||
var path = ReconstructPath(current);
|
||||
return new PathResult(PathStatus.Success, path, nodesExplored, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
foreach (var move in _allMoves)
|
||||
{
|
||||
moveResult.Cost = 0;
|
||||
move.Calculate(ctx, current.X, current.Y, current.Z, ref moveResult);
|
||||
|
||||
if (moveResult.IsImpossible)
|
||||
continue;
|
||||
|
||||
int nx = moveResult.DestX;
|
||||
int ny = moveResult.DestY;
|
||||
int nz = moveResult.DestZ;
|
||||
|
||||
if (!ctx.IsChunkLoaded(nx, nz))
|
||||
{
|
||||
unloadedChunkHits++;
|
||||
if (unloadedChunkHits > _maxChunkBorderFetch)
|
||||
continue;
|
||||
}
|
||||
|
||||
double tentativeG = current.GCost + moveResult.Cost;
|
||||
long packed = PathNode.Pack(nx, ny, nz);
|
||||
|
||||
if (nodeMap.TryGetValue(packed, out var neighbor))
|
||||
{
|
||||
if (neighbor.IsClosed)
|
||||
continue;
|
||||
if (tentativeG >= neighbor.GCost)
|
||||
continue;
|
||||
|
||||
neighbor.GCost = tentativeG;
|
||||
neighbor.Parent = current;
|
||||
neighbor.MoveUsed = move.Type;
|
||||
if (neighbor.IsOpen)
|
||||
openSet.Update(neighbor);
|
||||
}
|
||||
else
|
||||
{
|
||||
neighbor = new PathNode(nx, ny, nz)
|
||||
{
|
||||
GCost = tentativeG,
|
||||
HCost = goal.Heuristic(nx, ny, nz),
|
||||
Parent = current,
|
||||
MoveUsed = move.Type,
|
||||
IsOpen = true
|
||||
};
|
||||
nodeMap[packed] = neighbor;
|
||||
openSet.Insert(neighbor);
|
||||
}
|
||||
|
||||
double partialScore = neighbor.HCost + neighbor.GCost * 0.5;
|
||||
if (partialScore < bestPartialScore)
|
||||
{
|
||||
bestPartialScore = partialScore;
|
||||
bestPartialNode = neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPartialNode is not null && bestPartialNode != startNode)
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Partial path to ({bestPartialNode.X},{bestPartialNode.Y},{bestPartialNode.Z}), " +
|
||||
$"{nodesExplored} nodes, {sw.ElapsedMilliseconds}ms");
|
||||
var path = ReconstructPath(bestPartialNode);
|
||||
return new PathResult(PathStatus.Partial, path, nodesExplored, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
DebugLog?.Invoke($"[A*] Failed, {nodesExplored} nodes, {sw.ElapsedMilliseconds}ms");
|
||||
return PathResult.Fail(nodesExplored, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
private static List<PathNode> ReconstructPath(PathNode end)
|
||||
{
|
||||
var path = new List<PathNode>();
|
||||
var current = end;
|
||||
while (current is not null)
|
||||
{
|
||||
path.Add(current);
|
||||
current = current.Parent;
|
||||
}
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
MinecraftClient/Pathing/Core/ActionCosts.cs
Normal file
63
MinecraftClient/Pathing/Core/ActionCosts.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// All pathfinding movement costs in ticks, derived from vanilla walking/sprinting speeds.
|
||||
/// Mirrors Baritone's ActionCosts design.
|
||||
/// </summary>
|
||||
public static class ActionCosts
|
||||
{
|
||||
public const double WalkOneBlock = 20.0 / 4.317;
|
||||
public const double SprintOneBlock = 20.0 / 5.612;
|
||||
public const double SneakOneBlock = 20.0 / 1.3;
|
||||
public const double LadderUpOne = 20.0 / 2.35;
|
||||
public const double LadderDownOne = 20.0 / 3.0;
|
||||
public const double WalkOffBlock = WalkOneBlock * 0.8;
|
||||
public const double SprintMultiplier = SprintOneBlock / WalkOneBlock;
|
||||
public const double DiagonalMultiplier = 1.4142135623730951;
|
||||
public const double CostInf = 1_000_000;
|
||||
|
||||
public const double JumpPenalty = 2.0;
|
||||
|
||||
public static readonly double[] FallNBlocksCost = BuildFallTable(257);
|
||||
|
||||
private static double[] BuildFallTable(int maxBlocks)
|
||||
{
|
||||
var table = new double[maxBlocks];
|
||||
table[0] = 0;
|
||||
|
||||
double velocity = 0;
|
||||
double distance = 0;
|
||||
int ticks = 0;
|
||||
int blockIndex = 1;
|
||||
|
||||
while (blockIndex < maxBlocks)
|
||||
{
|
||||
velocity += 0.08;
|
||||
velocity *= 0.98;
|
||||
distance += velocity;
|
||||
ticks++;
|
||||
|
||||
while (blockIndex < maxBlocks && distance >= blockIndex)
|
||||
{
|
||||
table[blockIndex] = ticks;
|
||||
blockIndex++;
|
||||
}
|
||||
|
||||
if (ticks > 10000)
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = blockIndex; i < maxBlocks; i++)
|
||||
table[i] = CostInf;
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
public static double FallCost(int blocks)
|
||||
{
|
||||
if (blocks < 0 || blocks >= FallNBlocksCost.Length)
|
||||
return CostInf;
|
||||
return FallNBlocksCost[blocks];
|
||||
}
|
||||
}
|
||||
}
|
||||
96
MinecraftClient/Pathing/Core/BinaryHeapOpenSet.cs
Normal file
96
MinecraftClient/Pathing/Core/BinaryHeapOpenSet.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Min-heap of PathNodes ordered by FCost, used as the A* open set.
|
||||
/// </summary>
|
||||
public sealed class BinaryHeapOpenSet
|
||||
{
|
||||
private PathNode[] _heap;
|
||||
private int _size;
|
||||
|
||||
public int Count => _size;
|
||||
|
||||
public BinaryHeapOpenSet(int initialCapacity = 1024)
|
||||
{
|
||||
_heap = new PathNode[initialCapacity];
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
public void Insert(PathNode node)
|
||||
{
|
||||
if (_size == _heap.Length)
|
||||
Array.Resize(ref _heap, _heap.Length * 2);
|
||||
|
||||
node.HeapIndex = _size;
|
||||
_heap[_size] = node;
|
||||
_size++;
|
||||
SiftUp(_size - 1);
|
||||
}
|
||||
|
||||
public PathNode RemoveMin()
|
||||
{
|
||||
var min = _heap[0];
|
||||
_size--;
|
||||
if (_size > 0)
|
||||
{
|
||||
_heap[0] = _heap[_size];
|
||||
_heap[0].HeapIndex = 0;
|
||||
SiftDown(0);
|
||||
}
|
||||
_heap[_size] = null!;
|
||||
min.IsOpen = false;
|
||||
return min;
|
||||
}
|
||||
|
||||
public void Update(PathNode node)
|
||||
{
|
||||
SiftUp(node.HeapIndex);
|
||||
}
|
||||
|
||||
private void SiftUp(int i)
|
||||
{
|
||||
var node = _heap[i];
|
||||
while (i > 0)
|
||||
{
|
||||
int parent = (i - 1) >> 1;
|
||||
if (Compare(node, _heap[parent]) >= 0)
|
||||
break;
|
||||
_heap[i] = _heap[parent];
|
||||
_heap[i].HeapIndex = i;
|
||||
i = parent;
|
||||
}
|
||||
_heap[i] = node;
|
||||
node.HeapIndex = i;
|
||||
}
|
||||
|
||||
private void SiftDown(int i)
|
||||
{
|
||||
var node = _heap[i];
|
||||
int half = _size >> 1;
|
||||
while (i < half)
|
||||
{
|
||||
int left = (i << 1) + 1;
|
||||
int right = left + 1;
|
||||
int best = left;
|
||||
if (right < _size && Compare(_heap[right], _heap[left]) < 0)
|
||||
best = right;
|
||||
if (Compare(node, _heap[best]) <= 0)
|
||||
break;
|
||||
_heap[i] = _heap[best];
|
||||
_heap[i].HeapIndex = i;
|
||||
i = best;
|
||||
}
|
||||
_heap[i] = node;
|
||||
node.HeapIndex = i;
|
||||
}
|
||||
|
||||
private static int Compare(PathNode a, PathNode b)
|
||||
{
|
||||
int cmp = a.FCost.CompareTo(b.FCost);
|
||||
if (cmp != 0) return cmp;
|
||||
return a.HCost.CompareTo(b.HCost);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
MinecraftClient/Pathing/Core/CalculationContext.cs
Normal file
67
MinecraftClient/Pathing/Core/CalculationContext.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Moves;
|
||||
|
||||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe snapshot of world state and player capabilities for path planning.
|
||||
/// Created once at the start of a search; all move calculations read from this.
|
||||
/// </summary>
|
||||
public sealed class CalculationContext
|
||||
{
|
||||
public World World { get; }
|
||||
public bool CanSprint { get; }
|
||||
public bool AllowParkour { get; }
|
||||
public bool AllowParkourAscend { get; }
|
||||
public bool AllowDiagonalDescend { get; }
|
||||
public int MaxFallHeight { get; }
|
||||
public double JumpPenalty { get; }
|
||||
public double WalkCost { get; }
|
||||
public double SprintCost { get; }
|
||||
public double SneakCost { get; }
|
||||
|
||||
public CalculationContext(
|
||||
World world,
|
||||
bool canSprint = true,
|
||||
bool allowParkour = false,
|
||||
bool allowParkourAscend = false,
|
||||
bool allowDiagonalDescend = true,
|
||||
int maxFallHeight = 3,
|
||||
double jumpPenalty = ActionCosts.JumpPenalty)
|
||||
{
|
||||
World = world;
|
||||
CanSprint = canSprint;
|
||||
AllowParkour = allowParkour;
|
||||
AllowParkourAscend = allowParkourAscend;
|
||||
AllowDiagonalDescend = allowDiagonalDescend;
|
||||
MaxFallHeight = maxFallHeight;
|
||||
JumpPenalty = jumpPenalty;
|
||||
WalkCost = ActionCosts.WalkOneBlock;
|
||||
SprintCost = CanSprint ? ActionCosts.SprintOneBlock : ActionCosts.WalkOneBlock;
|
||||
SneakCost = ActionCosts.SneakOneBlock;
|
||||
}
|
||||
|
||||
public Block GetBlock(int x, int y, int z)
|
||||
=> World.GetBlock(new Location(x, y, z));
|
||||
|
||||
public Material GetMaterial(int x, int y, int z)
|
||||
=> GetBlock(x, y, z).Type;
|
||||
|
||||
public bool CanWalkThrough(int x, int y, int z)
|
||||
=> MoveHelper.CanWalkThrough(this, x, y, z);
|
||||
|
||||
public bool CanWalkOn(int x, int y, int z)
|
||||
=> MoveHelper.CanWalkOn(this, x, y, z);
|
||||
|
||||
public bool IsFullyPassable(int x, int y, int z)
|
||||
=> MoveHelper.IsFullyPassable(this, x, y, z);
|
||||
|
||||
public bool IsChunkLoaded(int x, int z)
|
||||
{
|
||||
int cx = x >> 4;
|
||||
int cz = z >> 4;
|
||||
var col = World[cx, cz];
|
||||
return col is not null && col.FullyLoaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
MinecraftClient/Pathing/Core/MoveResult.cs
Normal file
28
MinecraftClient/Pathing/Core/MoveResult.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of an IMove.Calculate() call. Mutable struct passed by ref for zero-alloc hot path.
|
||||
/// </summary>
|
||||
public struct MoveResult
|
||||
{
|
||||
public int DestX;
|
||||
public int DestY;
|
||||
public int DestZ;
|
||||
public double Cost;
|
||||
|
||||
public void Set(int x, int y, int z, double cost)
|
||||
{
|
||||
DestX = x;
|
||||
DestY = y;
|
||||
DestZ = z;
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public void SetImpossible()
|
||||
{
|
||||
Cost = ActionCosts.CostInf;
|
||||
}
|
||||
|
||||
public readonly bool IsImpossible => Cost >= ActionCosts.CostInf;
|
||||
}
|
||||
}
|
||||
13
MinecraftClient/Pathing/Core/MoveType.cs
Normal file
13
MinecraftClient/Pathing/Core/MoveType.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
public enum MoveType
|
||||
{
|
||||
Traverse,
|
||||
Diagonal,
|
||||
Ascend,
|
||||
Descend,
|
||||
Fall,
|
||||
Climb,
|
||||
Parkour
|
||||
}
|
||||
}
|
||||
39
MinecraftClient/Pathing/Core/PathNode.cs
Normal file
39
MinecraftClient/Pathing/Core/PathNode.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A* search node. Stored in the open/closed sets during pathfinding.
|
||||
/// </summary>
|
||||
public sealed class PathNode
|
||||
{
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly int Z;
|
||||
|
||||
public double GCost;
|
||||
public double HCost;
|
||||
public double FCost => GCost + HCost;
|
||||
|
||||
public PathNode? Parent;
|
||||
public MoveType MoveUsed;
|
||||
|
||||
public int HeapIndex;
|
||||
public bool IsOpen;
|
||||
public bool IsClosed;
|
||||
|
||||
public PathNode(int x, int y, int z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public long PackedPosition => Pack(X, Y, Z);
|
||||
|
||||
public static long Pack(int x, int y, int z)
|
||||
{
|
||||
return ((long)(x + 30_000_000) << 36)
|
||||
| ((long)(z + 30_000_000) << 12)
|
||||
| (long)((y + 64) & 0xFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MinecraftClient/Pathing/Core/PathResult.cs
Normal file
30
MinecraftClient/Pathing/Core/PathResult.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Pathing.Core
|
||||
{
|
||||
public enum PathStatus
|
||||
{
|
||||
Success,
|
||||
Partial,
|
||||
Failed
|
||||
}
|
||||
|
||||
public sealed class PathResult
|
||||
{
|
||||
public PathStatus Status { get; }
|
||||
public IReadOnlyList<PathNode> Path { get; }
|
||||
public int NodesExplored { get; }
|
||||
public long ElapsedMs { get; }
|
||||
|
||||
public PathResult(PathStatus status, IReadOnlyList<PathNode> path, int nodesExplored, long elapsedMs)
|
||||
{
|
||||
Status = status;
|
||||
Path = path;
|
||||
NodesExplored = nodesExplored;
|
||||
ElapsedMs = elapsedMs;
|
||||
}
|
||||
|
||||
public static PathResult Fail(int nodesExplored, long elapsedMs)
|
||||
=> new(PathStatus.Failed, [], nodesExplored, elapsedMs);
|
||||
}
|
||||
}
|
||||
42
MinecraftClient/Pathing/Goals/GoalBlock.cs
Normal file
42
MinecraftClient/Pathing/Goals/GoalBlock.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Pathing.Goals
|
||||
{
|
||||
public sealed class GoalBlock : IGoal
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Z { get; }
|
||||
|
||||
public GoalBlock(int x, int y, int z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public bool IsInGoal(int x, int y, int z)
|
||||
=> x == X && y == Y && z == Z;
|
||||
|
||||
public double Heuristic(int x, int y, int z)
|
||||
{
|
||||
int dx = Math.Abs(x - X);
|
||||
int dy = Math.Abs(y - Y);
|
||||
int dz = Math.Abs(z - Z);
|
||||
return DistanceHeuristic(dx, dy, dz);
|
||||
}
|
||||
|
||||
internal static double DistanceHeuristic(int dx, int dy, int dz)
|
||||
{
|
||||
int horizontal = Math.Max(dx, dz);
|
||||
int diagonal = Math.Min(dx, dz);
|
||||
int straight = horizontal - diagonal;
|
||||
double cost = diagonal * Core.ActionCosts.SprintOneBlock * Core.ActionCosts.DiagonalMultiplier
|
||||
+ straight * Core.ActionCosts.SprintOneBlock
|
||||
+ Math.Abs(dy) * Core.ActionCosts.SprintOneBlock;
|
||||
return cost;
|
||||
}
|
||||
|
||||
public override string ToString() => $"GoalBlock({X}, {Y}, {Z})";
|
||||
}
|
||||
}
|
||||
45
MinecraftClient/Pathing/Goals/GoalComposite.cs
Normal file
45
MinecraftClient/Pathing/Goals/GoalComposite.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Pathing.Goals
|
||||
{
|
||||
public sealed class GoalComposite : IGoal
|
||||
{
|
||||
private readonly IGoal[] _goals;
|
||||
|
||||
public GoalComposite(params IGoal[] goals)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(goals);
|
||||
_goals = goals;
|
||||
}
|
||||
|
||||
public GoalComposite(IEnumerable<IGoal> goals)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(goals);
|
||||
_goals = goals is IGoal[] arr ? arr : [.. goals];
|
||||
}
|
||||
|
||||
public bool IsInGoal(int x, int y, int z)
|
||||
{
|
||||
foreach (var g in _goals)
|
||||
{
|
||||
if (g.IsInGoal(x, y, z))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public double Heuristic(int x, int y, int z)
|
||||
{
|
||||
double min = double.MaxValue;
|
||||
foreach (var g in _goals)
|
||||
{
|
||||
double h = g.Heuristic(x, y, z);
|
||||
if (h < min) min = h;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
public override string ToString() => $"GoalComposite({_goals.Length} goals)";
|
||||
}
|
||||
}
|
||||
42
MinecraftClient/Pathing/Goals/GoalNear.cs
Normal file
42
MinecraftClient/Pathing/Goals/GoalNear.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Pathing.Goals
|
||||
{
|
||||
public sealed class GoalNear : IGoal
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Z { get; }
|
||||
public int Range { get; }
|
||||
private readonly int _rangeSq;
|
||||
|
||||
public GoalNear(int x, int y, int z, int range)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
Range = range;
|
||||
_rangeSq = range * range;
|
||||
}
|
||||
|
||||
public bool IsInGoal(int x, int y, int z)
|
||||
{
|
||||
int dx = x - X;
|
||||
int dy = y - Y;
|
||||
int dz = z - Z;
|
||||
return dx * dx + dy * dy + dz * dz <= _rangeSq;
|
||||
}
|
||||
|
||||
public double Heuristic(int x, int y, int z)
|
||||
{
|
||||
int dx = Math.Abs(x - X);
|
||||
int dy = Math.Abs(y - Y);
|
||||
int dz = Math.Abs(z - Z);
|
||||
double h = GoalBlock.DistanceHeuristic(dx, dy, dz);
|
||||
double reduction = Range * Core.ActionCosts.SprintOneBlock;
|
||||
return Math.Max(0, h - reduction);
|
||||
}
|
||||
|
||||
public override string ToString() => $"GoalNear({X}, {Y}, {Z}, range={Range})";
|
||||
}
|
||||
}
|
||||
28
MinecraftClient/Pathing/Goals/GoalXZ.cs
Normal file
28
MinecraftClient/Pathing/Goals/GoalXZ.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Pathing.Goals
|
||||
{
|
||||
public sealed class GoalXZ : IGoal
|
||||
{
|
||||
public int X { get; }
|
||||
public int Z { get; }
|
||||
|
||||
public GoalXZ(int x, int z)
|
||||
{
|
||||
X = x;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public bool IsInGoal(int x, int y, int z)
|
||||
=> x == X && z == Z;
|
||||
|
||||
public double Heuristic(int x, int y, int z)
|
||||
{
|
||||
int dx = Math.Abs(x - X);
|
||||
int dz = Math.Abs(z - Z);
|
||||
return GoalBlock.DistanceHeuristic(dx, 0, dz);
|
||||
}
|
||||
|
||||
public override string ToString() => $"GoalXZ({X}, {Z})";
|
||||
}
|
||||
}
|
||||
8
MinecraftClient/Pathing/Goals/IGoal.cs
Normal file
8
MinecraftClient/Pathing/Goals/IGoal.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace MinecraftClient.Pathing.Goals
|
||||
{
|
||||
public interface IGoal
|
||||
{
|
||||
bool IsInGoal(int x, int y, int z);
|
||||
double Heuristic(int x, int y, int z);
|
||||
}
|
||||
}
|
||||
18
MinecraftClient/Pathing/Moves/IMove.cs
Normal file
18
MinecraftClient/Pathing/Moves/IMove.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents one type of movement action for path planning.
|
||||
/// Each implementation defines its spatial check pattern and cost model.
|
||||
/// </summary>
|
||||
public interface IMove
|
||||
{
|
||||
MoveType Type { get; }
|
||||
int XOffset { get; }
|
||||
int ZOffset { get; }
|
||||
bool DynamicY { get; }
|
||||
|
||||
void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result);
|
||||
}
|
||||
}
|
||||
50
MinecraftClient/Pathing/Moves/Impl/MoveAscend.cs
Normal file
50
MinecraftClient/Pathing/Moves/Impl/MoveAscend.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Jump up 1 block in a cardinal direction.
|
||||
/// Requires: headroom at (x, y+2, z), body space at dest (y+1, y+2), ground at dest (y).
|
||||
/// </summary>
|
||||
public sealed class MoveAscend : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Ascend;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveAscend(int xOffset, int zOffset)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
int destX = x + XOffset;
|
||||
int destZ = z + ZOffset;
|
||||
int destY = y + 1;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) || !ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
MinecraftClient/Pathing/Moves/Impl/MoveClimb.cs
Normal file
64
MinecraftClient/Pathing/Moves/Impl/MoveClimb.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Climb up or down a ladder/vine at the current X,Z position.
|
||||
/// </summary>
|
||||
public sealed class MoveClimb : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Climb;
|
||||
public int XOffset => 0;
|
||||
public int ZOffset => 0;
|
||||
public bool DynamicY => false;
|
||||
|
||||
private readonly bool _up;
|
||||
|
||||
public MoveClimb(bool up)
|
||||
{
|
||||
_up = up;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
var currentMat = ctx.GetMaterial(x, y, z);
|
||||
if (!MoveHelper.IsClimbable(currentMat))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_up)
|
||||
{
|
||||
int destY = y + 1;
|
||||
if (!ctx.CanWalkThrough(x, destY + 1, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
var aboveMat = ctx.GetMaterial(x, destY, z);
|
||||
if (MoveHelper.IsClimbable(aboveMat) || !ctx.GetMaterial(x, destY, z).IsSolid())
|
||||
{
|
||||
result.Set(x, destY, z, ActionCosts.LadderUpOne);
|
||||
return;
|
||||
}
|
||||
|
||||
result.SetImpossible();
|
||||
}
|
||||
else
|
||||
{
|
||||
int destY = y - 1;
|
||||
var belowMat = ctx.GetMaterial(x, destY, z);
|
||||
if (MoveHelper.IsClimbable(belowMat) || !belowMat.IsSolid())
|
||||
{
|
||||
result.Set(x, destY, z, ActionCosts.LadderDownOne);
|
||||
return;
|
||||
}
|
||||
|
||||
result.SetImpossible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
MinecraftClient/Pathing/Moves/Impl/MoveDescend.cs
Normal file
66
MinecraftClient/Pathing/Moves/Impl/MoveDescend.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Walk off a ledge and drop 1-N blocks in a cardinal direction.
|
||||
/// Scans downward for a landing spot within MaxFallHeight.
|
||||
/// </summary>
|
||||
public sealed class MoveDescend : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Descend;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => true;
|
||||
|
||||
public MoveDescend(int xOffset, int zOffset)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
int destX = x + XOffset;
|
||||
int destZ = z + ZOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int fallDist = 1; fallDist <= ctx.MaxFallHeight; fallDist++)
|
||||
{
|
||||
int landY = y - fallDist;
|
||||
|
||||
if (ctx.CanWalkOn(destX, landY - 1, destZ))
|
||||
{
|
||||
if (!ctx.CanWalkThrough(destX, landY, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ActionCosts.WalkOffBlock + ActionCosts.FallCost(fallDist);
|
||||
if (MoveHelper.IsHazardous(ctx.GetMaterial(destX, landY - 1, destZ)))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
result.Set(destX, landY, destZ, cost);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, landY, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result.SetImpossible();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
MinecraftClient/Pathing/Moves/Impl/MoveDiagonal.cs
Normal file
54
MinecraftClient/Pathing/Moves/Impl/MoveDiagonal.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagonal walk (1 block in both X and Z, same Y).
|
||||
/// Checks both intermediate cardinal columns for clearance.
|
||||
/// </summary>
|
||||
public sealed class MoveDiagonal : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Diagonal;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveDiagonal(int xOffset, int zOffset)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
int destX = x + XOffset;
|
||||
int destZ = z + ZOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(x + XOffset, y, z) || !ctx.CanWalkThrough(x + XOffset, y + 1, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y, z + ZOffset) || !ctx.CanWalkThrough(x, y + 1, z + ZOffset))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
result.Set(destX, y, destZ, ctx.SprintCost * ActionCosts.DiagonalMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
MinecraftClient/Pathing/Moves/Impl/MoveTraverse.cs
Normal file
54
MinecraftClient/Pathing/Moves/Impl/MoveTraverse.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Flat cardinal walk (1 block in +/-X or +/-Z, same Y).
|
||||
/// Checks body+head passable and ground below destination.
|
||||
/// </summary>
|
||||
public sealed class MoveTraverse : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Traverse;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveTraverse(int xOffset, int zOffset)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
int destX = x + XOffset;
|
||||
int destZ = z + ZOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost;
|
||||
|
||||
var destFloorMat = ctx.GetMaterial(destX, y - 1, destZ);
|
||||
if (destFloorMat == Mapping.Material.SoulSand)
|
||||
cost *= 1.0 / Physics.PhysicsConsts.SoulSandSpeedFactor;
|
||||
|
||||
result.Set(destX, y, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
MinecraftClient/Pathing/Moves/MoveHelper.cs
Normal file
69
MinecraftClient/Pathing/Moves/MoveHelper.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves
|
||||
{
|
||||
/// <summary>
|
||||
/// Block passability checks for path planning.
|
||||
/// Uses Material-level checks initially; designed to allow future BlockShapes upgrade.
|
||||
/// </summary>
|
||||
public static class MoveHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Can a player's body/head occupy this block position? (air, open door, tall grass, etc.)
|
||||
/// </summary>
|
||||
public static bool CanWalkThrough(CalculationContext ctx, int x, int y, int z)
|
||||
{
|
||||
Material mat = ctx.GetMaterial(x, y, z);
|
||||
if (mat == Material.Air || mat == Material.CaveAir || mat == Material.VoidAir)
|
||||
return true;
|
||||
if (mat.IsLiquid())
|
||||
return false;
|
||||
if (mat.IsSolid())
|
||||
return false;
|
||||
if (mat.CanHarmPlayers())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can a player stand on top of this block? (solid upper surface)
|
||||
/// </summary>
|
||||
public static bool CanWalkOn(CalculationContext ctx, int x, int y, int z)
|
||||
{
|
||||
Material mat = ctx.GetMaterial(x, y, z);
|
||||
if (mat == Material.Air || mat == Material.CaveAir || mat == Material.VoidAir)
|
||||
return false;
|
||||
if (mat.IsLiquid())
|
||||
return false;
|
||||
if (mat.CanHarmPlayers())
|
||||
return false;
|
||||
return mat.IsSolid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this block completely passable with no slowdown or interaction?
|
||||
/// Stricter than CanWalkThrough -- excludes water, cobwebs, etc.
|
||||
/// </summary>
|
||||
public static bool IsFullyPassable(CalculationContext ctx, int x, int y, int z)
|
||||
{
|
||||
Material mat = ctx.GetMaterial(x, y, z);
|
||||
return mat == Material.Air || mat == Material.CaveAir || mat == Material.VoidAir;
|
||||
}
|
||||
|
||||
public static bool IsClimbable(Material mat)
|
||||
{
|
||||
return mat.CanBeClimbedOn();
|
||||
}
|
||||
|
||||
public static bool IsHazardous(Material mat)
|
||||
{
|
||||
return mat.CanHarmPlayers();
|
||||
}
|
||||
|
||||
public static bool IsWater(Material mat)
|
||||
{
|
||||
return mat == Material.Water;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4585,6 +4585,24 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use new A* pathfinding to navigate to a location..
|
||||
/// </summary>
|
||||
internal static string cmd_pathfind_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.pathfind.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pathfinding to ({0}, {1}, {2})....
|
||||
/// </summary>
|
||||
internal static string cmd_pathfind_started {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.pathfind.started", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to restart and reconnect to the server..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1538,6 +1538,12 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="cmd.move.walk" xml:space="preserve">
|
||||
<value>Walking from {1} to {0}</value>
|
||||
</data>
|
||||
<data name="cmd.pathfind.desc" xml:space="preserve">
|
||||
<value>Use new A* pathfinding to navigate to a location.</value>
|
||||
</data>
|
||||
<data name="cmd.pathfind.started" xml:space="preserve">
|
||||
<value>Pathfinding to ({0}, {1}, {2})...</value>
|
||||
</data>
|
||||
<data name="cmd.reco.desc" xml:space="preserve">
|
||||
<value>restart and reconnect to the server.</value>
|
||||
</data>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue