mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add /goto command with A* pathfinder integration and MoveFall
- Add MoveFall move for straight-down falls beyond MoveDescend range - Register MoveFall in default move set - Create /goto command using new A* pathfinder - Add MoveToAStar() method to McClient bridging A* results to existing path execution system (Queue<Location> + UpdatePathfindingInput) - Add translation entries for goto command Made-with: Cursor
This commit is contained in:
parent
deb1bc47cc
commit
77c5f88168
6 changed files with 217 additions and 0 deletions
50
MinecraftClient/Commands/Goto.cs
Normal file
50
MinecraftClient/Commands/Goto.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Mapping;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Goto : Command
|
||||
{
|
||||
public override string CmdName => "goto";
|
||||
public override string CmdUsage => "goto <x y z>";
|
||||
public override string CmdDesc => Translations.cmd_goto_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty)))
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Then(l => l.Argument("location", MccArguments.Location())
|
||||
.Executes(r => DoGoto(r.Source, MccArguments.GetLocation(r, "location"))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string? cmd)
|
||||
{
|
||||
return r.SetAndReturn(GetCmdDescTranslated());
|
||||
}
|
||||
|
||||
private static int DoGoto(CmdResult r, Location goal)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetTerrainEnabled())
|
||||
return r.SetAndReturn(Status.FailNeedTerrain);
|
||||
|
||||
Location current = handler.GetCurrentLocation();
|
||||
goal.ToAbsolute(current);
|
||||
|
||||
var (success, message) = handler.MoveToAStar(goal);
|
||||
|
||||
return r.SetAndReturn(success ? Status.Done : Status.Fail, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1714,6 +1714,66 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a goal using the new A* pathfinder.
|
||||
/// Runs the search, converts the result into the legacy Queue path, and starts movement.
|
||||
/// Returns a description of the result for UI feedback.
|
||||
/// </summary>
|
||||
public (bool success, string message) MoveToAStar(Location goal, long timeoutMs = 5000)
|
||||
{
|
||||
lock (locationLock)
|
||||
{
|
||||
var ctx = new Pathing.Core.CalculationContext(world);
|
||||
var finder = new Pathing.Core.AStarPathFinder();
|
||||
finder.DebugLog = msg => Log.Debug(msg);
|
||||
|
||||
int sx = (int)Math.Floor(location.X);
|
||||
int sy = (int)Math.Floor(location.Y);
|
||||
int sz = (int)Math.Floor(location.Z);
|
||||
int gx = (int)Math.Floor(goal.X);
|
||||
int gy = (int)Math.Floor(goal.Y);
|
||||
int gz = (int)Math.Floor(goal.Z);
|
||||
|
||||
Log.Info($"[Goto] A* search from ({sx},{sy},{sz}) to ({gx},{gy},{gz})...");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz,
|
||||
new Pathing.Goals.GoalBlock(gx, gy, gz), cts.Token, timeoutMs);
|
||||
|
||||
Log.Info($"[Goto] A* result: {result.Status}, nodes={result.NodesExplored}, " +
|
||||
$"time={result.ElapsedMs}ms, path length={result.Path.Count}");
|
||||
|
||||
if (result.Status == Pathing.Core.PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
return (false, string.Format(Translations.cmd_goto_failed,
|
||||
result.NodesExplored, result.ElapsedMs));
|
||||
}
|
||||
|
||||
var queue = new Queue<Location>();
|
||||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
queue.Enqueue(new Location(node.X + 0.5, node.Y, node.Z + 0.5));
|
||||
}
|
||||
|
||||
Log.Info($"[Goto] Path waypoints: {queue.Count}");
|
||||
int logCount = 0;
|
||||
foreach (var wp in queue)
|
||||
{
|
||||
if (logCount < 30 || logCount == queue.Count - 1)
|
||||
Log.Debug($"[Goto] wp[{logCount}] = ({wp.X:F1},{wp.Y:F1},{wp.Z:F1})");
|
||||
logCount++;
|
||||
}
|
||||
|
||||
pathTarget = null;
|
||||
path = queue;
|
||||
|
||||
string statusStr = result.Status == Pathing.Core.PathStatus.Partial ? " (partial)" : "";
|
||||
return (true, string.Format(Translations.cmd_goto_success,
|
||||
queue.Count, result.NodesExplored, result.ElapsedMs, statusStr));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat message or command to the server
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ namespace MinecraftClient.Pathing.Core
|
|||
moves.Add(new MoveClimb(true));
|
||||
moves.Add(new MoveClimb(false));
|
||||
|
||||
moves.Add(new MoveFall());
|
||||
|
||||
return [.. moves];
|
||||
}
|
||||
|
||||
|
|
|
|||
69
MinecraftClient/Pathing/Moves/Impl/MoveFall.cs
Normal file
69
MinecraftClient/Pathing/Moves/Impl/MoveFall.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Straight-down fall at the current X,Z position, for drops greater than MaxFallHeight
|
||||
/// that MoveDescend won't cover. Scans downward for a safe landing.
|
||||
/// </summary>
|
||||
public sealed class MoveFall : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Fall;
|
||||
public int XOffset => 0;
|
||||
public int ZOffset => 0;
|
||||
public bool DynamicY => true;
|
||||
|
||||
private readonly int _maxScanDepth;
|
||||
|
||||
public MoveFall(int maxScanDepth = 256)
|
||||
{
|
||||
_maxScanDepth = maxScanDepth;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
if (!ctx.CanWalkThrough(x, y - 1, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int fallDist = 1; fallDist <= _maxScanDepth; fallDist++)
|
||||
{
|
||||
int landY = y - fallDist;
|
||||
|
||||
if (ctx.CanWalkOn(x, landY - 1, z))
|
||||
{
|
||||
if (!ctx.CanWalkThrough(x, landY, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (MoveHelper.IsHazardous(ctx.GetMaterial(x, landY - 1, z)))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double fallDamageThreshold = 3;
|
||||
double cost = ActionCosts.FallCost(fallDist);
|
||||
|
||||
if (fallDist > fallDamageThreshold)
|
||||
cost += (fallDist - fallDamageThreshold) * 5.0;
|
||||
|
||||
result.Set(x, landY, z, cost);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(x, landY, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result.SetImpossible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3501,6 +3501,33 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to navigate to a location using A* pathfinding..
|
||||
/// </summary>
|
||||
internal static string cmd_goto_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.goto.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Path found: {0} waypoints, {1} nodes explored in {2}ms{3}.
|
||||
/// </summary>
|
||||
internal static string cmd_goto_success {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.goto.success", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No path found ({0} nodes explored in {1}ms).
|
||||
/// </summary>
|
||||
internal static string cmd_goto_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.goto.failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Already following {0}!.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1243,6 +1243,15 @@ Change EnableEmoji=false in the settings if the display is confusing.</value>
|
|||
<data name="cmd.exit.desc" xml:space="preserve">
|
||||
<value>disconnect from the server.</value>
|
||||
</data>
|
||||
<data name="cmd.goto.desc" xml:space="preserve">
|
||||
<value>navigate to a location using A* pathfinding.</value>
|
||||
</data>
|
||||
<data name="cmd.goto.success" xml:space="preserve">
|
||||
<value>Path found: {0} waypoints, {1} nodes explored in {2}ms{3}</value>
|
||||
</data>
|
||||
<data name="cmd.goto.failed" xml:space="preserve">
|
||||
<value>No path found ({0} nodes explored in {1}ms)</value>
|
||||
</data>
|
||||
<data name="cmd.follow.already_following" xml:space="preserve">
|
||||
<value>Already following {0}!</value>
|
||||
</data>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue