mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Add path execution telemetry and scenario runner
This commit is contained in:
parent
9fe376a3bb
commit
b9bff02107
8 changed files with 215 additions and 4 deletions
|
|
@ -0,0 +1,19 @@
|
|||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathTimingContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void Run_ManagerAcceptedAscendChain_CapturesPerSegmentTicks_AndZeroReplan()
|
||||
{
|
||||
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get("manager-accepted-ascend-chain");
|
||||
|
||||
PathingScenarioResult result = PathingScenarioRunner.RunAccepted(scenario);
|
||||
|
||||
Assert.Equal(0, result.ReplanCount);
|
||||
Assert.True(result.Completed);
|
||||
Assert.Equal(6, result.SegmentRuns.Count);
|
||||
Assert.All(result.SegmentRuns, run => Assert.True(run.ElapsedTicks > 0));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
internal sealed record PathingExecutionScenario
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required Func<World> BuildWorld { get; init; }
|
||||
public required Location Start { get; init; }
|
||||
public required GoalBlock Goal { get; init; }
|
||||
public required float StartYaw { get; init; }
|
||||
public required int MaxExecutionTicks { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
internal static class PathingExecutionScenarioCatalog
|
||||
{
|
||||
internal static PathingExecutionScenario Get(string scenarioId) => scenarioId switch
|
||||
{
|
||||
"manager-accepted-ascend-chain" => new PathingExecutionScenario
|
||||
{
|
||||
Id = scenarioId,
|
||||
BuildWorld = BuildManagerAcceptedAscendChain,
|
||||
Start = new Location(171.5, 80, 160.5),
|
||||
Goal = new GoalBlock(177, 83, 162),
|
||||
StartYaw = 315f,
|
||||
MaxExecutionTicks = 420
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(scenarioId), scenarioId, null)
|
||||
};
|
||||
|
||||
private static World BuildManagerAcceptedAscendChain()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 158, max: 180);
|
||||
FlatWorldTestBuilder.ClearBox(world, 170, 80, 160, 178, 85, 168);
|
||||
FlatWorldTestBuilder.SetSolid(world, 175, 80, 162);
|
||||
FlatWorldTestBuilder.SetSolid(world, 176, 81, 162);
|
||||
FlatWorldTestBuilder.SetSolid(world, 177, 82, 162);
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Threading;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
internal sealed record PathingScenarioResult(
|
||||
bool Completed,
|
||||
int ReplanCount,
|
||||
int TotalTicks,
|
||||
IReadOnlyList<PathSegmentRun> SegmentRuns,
|
||||
IReadOnlyList<string> DebugLogs,
|
||||
IReadOnlyList<string> InfoLogs,
|
||||
Location FinalPosition,
|
||||
PathResult PlanResult);
|
||||
|
||||
internal static class PathingScenarioRunner
|
||||
{
|
||||
internal static PathResult PlanOnly(PathingExecutionScenario scenario)
|
||||
{
|
||||
World world = scenario.BuildWorld();
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new AStarPathFinder();
|
||||
|
||||
return finder.Calculate(
|
||||
ctx,
|
||||
(int)Math.Floor(scenario.Start.X),
|
||||
(int)Math.Floor(scenario.Start.Y),
|
||||
(int)Math.Floor(scenario.Start.Z),
|
||||
scenario.Goal,
|
||||
CancellationToken.None,
|
||||
timeoutMs: 3000);
|
||||
}
|
||||
|
||||
internal static PathingScenarioResult RunAccepted(PathingExecutionScenario scenario)
|
||||
{
|
||||
World world = scenario.BuildWorld();
|
||||
var debugLogs = new List<string>();
|
||||
var infoLogs = new List<string>();
|
||||
var observer = new RecordingPathExecutionObserver();
|
||||
var manager = new PathSegmentManager(debugLogs.Add, infoLogs.Add, observer);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(scenario.Start, scenario.StartYaw);
|
||||
var input = new MovementInput();
|
||||
|
||||
PathResult planResult = PlanOnly(scenario);
|
||||
|
||||
manager.StartNavigation(scenario.Goal, planResult);
|
||||
|
||||
for (int tick = 0; tick < scenario.MaxExecutionTicks && manager.IsNavigating; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
manager.Tick(pos, physics, input, world);
|
||||
if (!manager.IsNavigating)
|
||||
break;
|
||||
|
||||
physics.ApplyInput(input);
|
||||
physics.Tick(world);
|
||||
}
|
||||
|
||||
Location finalPosition = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
bool completed = !manager.IsNavigating && manager.Goal is null && observer.TotalTicks > 0;
|
||||
|
||||
return new PathingScenarioResult(
|
||||
completed,
|
||||
observer.ReplanCount,
|
||||
observer.TotalTicks,
|
||||
observer.SegmentRuns.AsReadOnly(),
|
||||
debugLogs.AsReadOnly(),
|
||||
infoLogs.AsReadOnly(),
|
||||
finalPosition,
|
||||
planResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Pathing.Execution.Telemetry;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
internal sealed record PathSegmentRun(int SegmentIndex, MoveType MoveType, int ElapsedTicks, Location Position);
|
||||
|
||||
internal sealed class RecordingPathExecutionObserver : IPathExecutionObserver
|
||||
{
|
||||
internal List<PathSegmentRun> SegmentRuns { get; } = [];
|
||||
internal int ReplanCount { get; private set; }
|
||||
internal int TotalTicks { get; private set; }
|
||||
|
||||
public void OnNavigationStarted(IReadOnlyList<PathSegment> segments) { }
|
||||
|
||||
public void OnSegmentStarted(int segmentIndex, int totalSegments, PathSegment segment) { }
|
||||
|
||||
public void OnSegmentCompleted(int segmentIndex, int totalSegments, PathSegment segment, int elapsedTicks, Location position)
|
||||
=> SegmentRuns.Add(new PathSegmentRun(segmentIndex, segment.MoveType, elapsedTicks, position));
|
||||
|
||||
public void OnSegmentFailed(int segmentIndex, int totalSegments, PathSegment segment, int elapsedTicks, Location position)
|
||||
=> SegmentRuns.Add(new PathSegmentRun(segmentIndex, segment.MoveType, elapsedTicks, position));
|
||||
|
||||
public void OnNavigationCompleted(int totalTicks) => TotalTicks = totalTicks;
|
||||
|
||||
public void OnReplanStarted(int replanCount, Location position) => ReplanCount = replanCount;
|
||||
|
||||
public void OnReplanSucceeded(int replanCount, IReadOnlyList<PathSegment> segments) { }
|
||||
|
||||
public void OnReplanFailed(int replanCount, Location position) { }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue