Add path execution telemetry and scenario runner

This commit is contained in:
BruceChen 2026-04-13 16:36:11 +00:00
parent 9fe376a3bb
commit b9bff02107
8 changed files with 215 additions and 4 deletions

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Execution.Telemetry;
using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution
@ -22,18 +23,24 @@ namespace MinecraftClient.Pathing.Execution
private int _currentIndex;
private IActionTemplate? _currentTemplate;
private readonly Action<string>? _debugLog;
private readonly IPathExecutionObserver? _observer;
private int _segmentTicks;
private int _totalTicks;
public bool IsComplete => _currentIndex >= _segments.Count && _currentTemplate is null;
public int CurrentIndex => _currentIndex;
public int TotalSegments => _segments.Count;
public int TotalTicks => _totalTicks;
public PathSegment? CurrentSegment =>
_currentIndex < _segments.Count ? _segments[_currentIndex] : null;
public PathExecutor(List<PathSegment> segments, Action<string>? debugLog = null)
public PathExecutor(List<PathSegment> segments, Action<string>? debugLog = null, IPathExecutionObserver? observer = null)
{
_segments = segments;
_currentIndex = 0;
_debugLog = debugLog;
_observer = observer;
_observer?.OnNavigationStarted(segments);
AdvanceToNextSegment();
}
@ -45,15 +52,19 @@ namespace MinecraftClient.Pathing.Execution
return PathExecutorState.Complete;
}
_segmentTicks++;
_totalTicks++;
var state = _currentTemplate.Tick(pos, physics, input, world);
switch (state)
{
case TemplateState.Complete:
input.Reset();
_observer?.OnSegmentCompleted(_currentIndex, _segments.Count, _segments[_currentIndex], _segmentTicks, pos);
_debugLog?.Invoke($"[PathExec] Segment {_currentIndex} complete " +
$"({_segments[_currentIndex].MoveType}) at ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
_currentIndex++;
_segmentTicks = 0;
if (_currentIndex >= _segments.Count)
{
_currentTemplate = null;
@ -65,6 +76,7 @@ namespace MinecraftClient.Pathing.Execution
case TemplateState.Failed:
input.Reset();
_observer?.OnSegmentFailed(_currentIndex, _segments.Count, _segments[_currentIndex], _segmentTicks, pos);
_debugLog?.Invoke($"[PathExec] Segment {_currentIndex} FAILED " +
$"({_segments[_currentIndex].MoveType}) at ({pos.X:F2},{pos.Y:F2},{pos.Z:F2}), " +
$"target was ({_currentTemplate.ExpectedEnd.X:F2},{_currentTemplate.ExpectedEnd.Y:F2},{_currentTemplate.ExpectedEnd.Z:F2})");
@ -82,6 +94,7 @@ namespace MinecraftClient.Pathing.Execution
var seg = _segments[_currentIndex];
PathSegment? next = _currentIndex + 1 < _segments.Count ? _segments[_currentIndex + 1] : null;
_currentTemplate = ActionTemplateFactory.Create(seg, next);
_observer?.OnSegmentStarted(_currentIndex, _segments.Count, seg);
_debugLog?.Invoke($"[PathExec] Starting segment {_currentIndex}/{_segments.Count}: {seg}");
}
else

View file

@ -2,6 +2,7 @@ using System;
using System.Threading;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
using MinecraftClient.Pathing.Execution.Telemetry;
using MinecraftClient.Pathing.Goals;
using MinecraftClient.Physics;
@ -20,15 +21,17 @@ namespace MinecraftClient.Pathing.Execution
private readonly Action<string>? _debugLog;
private readonly Action<string>? _infoLog;
private readonly IPathExecutionObserver? _observer;
public bool IsNavigating => _executor is not null && !_executor.IsComplete;
public int ReplanCount => _replanCount;
public IGoal? Goal => _goal;
public PathSegmentManager(Action<string>? debugLog = null, Action<string>? infoLog = null)
public PathSegmentManager(Action<string>? debugLog = null, Action<string>? infoLog = null, IPathExecutionObserver? observer = null)
{
_debugLog = debugLog;
_infoLog = infoLog;
_observer = observer;
}
public void StartNavigation(IGoal goal, PathResult result)
@ -36,7 +39,7 @@ namespace MinecraftClient.Pathing.Execution
_goal = goal;
_replanCount = 0;
var segments = PathSegmentBuilder.FromPath(result.Path);
_executor = new PathExecutor(segments, _debugLog);
_executor = new PathExecutor(segments, _debugLog, _observer);
_infoLog?.Invoke($"[PathMgr] Navigation started: {segments.Count} segments");
}
@ -50,6 +53,7 @@ namespace MinecraftClient.Pathing.Execution
switch (state)
{
case PathExecutorState.Complete:
_observer?.OnNavigationCompleted(_executor.TotalTicks);
_infoLog?.Invoke("[PathMgr] Navigation complete!");
_executor = null;
_goal = null;
@ -75,8 +79,10 @@ namespace MinecraftClient.Pathing.Execution
private void Replan(Location pos, World world)
{
_replanCount++;
_observer?.OnReplanStarted(_replanCount, pos);
if (_replanCount > MaxReplans)
{
_observer?.OnReplanFailed(_replanCount, pos);
_infoLog?.Invoke($"[PathMgr] Giving up after {MaxReplans} replans.");
_executor = null;
_goal = null;
@ -117,6 +123,7 @@ namespace MinecraftClient.Pathing.Execution
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
{
_observer?.OnReplanFailed(_replanCount, pos);
_infoLog?.Invoke("[PathMgr] Replan failed -- no path found.");
_executor = null;
_goal = null;
@ -124,7 +131,8 @@ namespace MinecraftClient.Pathing.Execution
}
var segments = PathSegmentBuilder.FromPath(result.Path);
_executor = new PathExecutor(segments, _debugLog);
_observer?.OnReplanSucceeded(_replanCount, segments);
_executor = new PathExecutor(segments, _debugLog, _observer);
_infoLog?.Invoke($"[PathMgr] Replanned: {segments.Count} segments (replan #{_replanCount})");
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using MinecraftClient.Mapping;
namespace MinecraftClient.Pathing.Execution.Telemetry
{
public interface IPathExecutionObserver
{
void OnNavigationStarted(IReadOnlyList<PathSegment> segments);
void OnSegmentStarted(int segmentIndex, int totalSegments, PathSegment segment);
void OnSegmentCompleted(int segmentIndex, int totalSegments, PathSegment segment, int elapsedTicks, Location position);
void OnSegmentFailed(int segmentIndex, int totalSegments, PathSegment segment, int elapsedTicks, Location position);
void OnNavigationCompleted(int totalTicks);
void OnReplanStarted(int replanCount, Location position);
void OnReplanSucceeded(int replanCount, IReadOnlyList<PathSegment> segments);
void OnReplanFailed(int replanCount, Location position);
}
}