mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
test: add pathing transition regression coverage
This commit is contained in:
parent
360883acf3
commit
b0fab26c66
17 changed files with 1140 additions and 6 deletions
|
|
@ -0,0 +1,71 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
|
||||
public sealed class PathingContractStore
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, PathingPlannerContract> planners;
|
||||
private readonly IReadOnlyDictionary<string, PathingTimingBudget> timings;
|
||||
|
||||
private PathingContractStore(
|
||||
IReadOnlyDictionary<string, PathingPlannerContract> planners,
|
||||
IReadOnlyDictionary<string, PathingTimingBudget> timings)
|
||||
{
|
||||
this.planners = planners;
|
||||
this.timings = timings;
|
||||
}
|
||||
|
||||
public static PathingContractStore LoadFromRepositoryRoot()
|
||||
{
|
||||
string rootPath = FindRepositoryRoot();
|
||||
string pathingDir = Path.Combine(rootPath, "MinecraftClient.Tests", "TestData", "Pathing");
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
string plannerJson = File.ReadAllText(Path.Combine(pathingDir, "pathing-planner-contracts.json"));
|
||||
string timingJson = File.ReadAllText(Path.Combine(pathingDir, "pathing-timing-budgets.json"));
|
||||
|
||||
Dictionary<string, PathingPlannerContract> plannerContracts = JsonSerializer.Deserialize<Dictionary<string, PathingPlannerContract>>(plannerJson, options)
|
||||
?? throw new InvalidOperationException("Failed to deserialize planner contracts.");
|
||||
Dictionary<string, PathingTimingBudget> timingBudgets = JsonSerializer.Deserialize<Dictionary<string, PathingTimingBudget>>(timingJson, options)
|
||||
?? throw new InvalidOperationException("Failed to deserialize timing budgets.");
|
||||
|
||||
return new PathingContractStore(plannerContracts, timingBudgets);
|
||||
}
|
||||
|
||||
public PathingPlannerContract GetPlanner(string id)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
return planners.TryGetValue(id, out PathingPlannerContract? contract)
|
||||
? contract
|
||||
: throw new KeyNotFoundException($"Planner contract '{id}' was not found.");
|
||||
}
|
||||
|
||||
public PathingTimingBudget GetTiming(string id)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
return timings.TryGetValue(id, out PathingTimingBudget? budget)
|
||||
? budget
|
||||
: throw new KeyNotFoundException($"Timing budget '{id}' was not found.");
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "MinecraftClient.sln")))
|
||||
return current.FullName;
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Unable to locate repository root from current test execution directory.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
|
||||
public readonly record struct PathingBlock(int X, int Y, int Z);
|
||||
|
||||
public sealed record PathingPlannerSegmentContract(
|
||||
MoveType Move,
|
||||
PathingBlock From,
|
||||
PathingBlock To);
|
||||
|
||||
public sealed record PathingPlannerContract(
|
||||
PathStatus ExpectedStatus,
|
||||
PathingPlannerSegmentContract[] Segments);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
|
||||
public sealed record PathingSegmentTimingBudget(
|
||||
MoveType Move,
|
||||
int BudgetMs);
|
||||
|
||||
public sealed record PathingTimingBudget(
|
||||
int TotalBudgetMs,
|
||||
PathingSegmentTimingBudget[] Segments);
|
||||
|
|
@ -76,6 +76,7 @@ internal static class FlatWorldTestBuilder
|
|||
|
||||
public static void SetMaterial(World world, int x, int y, int z, Material material)
|
||||
{
|
||||
EnsureChunkColumn(world, x, z);
|
||||
world.SetBlock(new Location(x, y, z), new Block(ResolveMaterialId(material)));
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +99,14 @@ internal static class FlatWorldTestBuilder
|
|||
}
|
||||
}
|
||||
|
||||
private static void EnsureChunkColumn(World world, int x, int z)
|
||||
{
|
||||
int chunkX = (int)Math.Floor(x / 16.0);
|
||||
int chunkZ = (int)Math.Floor(z / 16.0);
|
||||
if (world[chunkX, chunkZ] is null)
|
||||
world[chunkX, chunkZ] = new ChunkColumn(24) { FullyLoaded = true };
|
||||
}
|
||||
|
||||
private static ushort ResolveMaterialId(Material material)
|
||||
{
|
||||
lock (InitLock)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class FlatWorldTestBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetSolid_CreatesMissingChunkColumns()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 118, max: 126);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 110);
|
||||
|
||||
Assert.Equal(Material.Stone, world.GetBlock(new Location(123, 79, 110)).Type);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
|
|
@ -30,6 +31,61 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTemplate_FinalStop_Completes_WhenCenterStopsInsideTargetBlockNearEdge()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(2.5, 80, 0.5),
|
||||
End = new Location(3.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop
|
||||
};
|
||||
|
||||
var template = new WalkTemplate(segment, null);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(3.2897, 80.0, 0.5),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
var input = new MovementInput();
|
||||
TemplateState state = template.Tick(new Location(physics.Position.X, physics.Position.Y, physics.Position.Z), physics, input, world);
|
||||
|
||||
Assert.Equal(TemplateState.Complete, state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTemplate_FinalStop_Completes_FromLiveNearGoalState_WithoutFailure()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(102.5, 80, 100.5),
|
||||
End = new Location(103.5, 80, 100.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop
|
||||
};
|
||||
|
||||
var template = new WalkTemplate(segment, null);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(103.36, 80.0, 100.50),
|
||||
DeltaMovement = new Vec3d(0.0346, 0.0, 0.0),
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 40, out _);
|
||||
|
||||
Assert.Equal(TemplateState.Complete, state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTemplate_PrepareJump_CompletesWithoutSettlingOnRunUpBlock()
|
||||
{
|
||||
|
|
@ -59,6 +115,42 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
Assert.True(physics.DeltaMovement.X > 0.02);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTemplate_TurnIntoParkour_CompletesOnlyWhenTurnEntryIsSlowAndJumpReady()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 128);
|
||||
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(121.5, 80, 110.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.Turn,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.08, 0.16, false, true, true, true, 12)
|
||||
};
|
||||
var next = new PathSegment
|
||||
{
|
||||
Start = new Location(121.5, 80, 110.5),
|
||||
End = new Location(121.5, 80, 111.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.PrepareJump,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.12, double.PositiveInfinity, false, true, true, false, 10),
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
var template = new WalkTemplate(current, next);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 270f);
|
||||
|
||||
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 140, out Location finalPos);
|
||||
double horizontalSpeed = Math.Sqrt(physics.DeltaMovement.X * physics.DeltaMovement.X + physics.DeltaMovement.Z * physics.DeltaMovement.Z);
|
||||
|
||||
Assert.Equal(TemplateState.Complete, state);
|
||||
Assert.True(
|
||||
TemplateFootingHelper.IsCenterInsideSupportStrip(finalPos, current.End, next.End),
|
||||
$"finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
Assert.InRange(horizontalSpeed, 0.08, 0.20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DescendTemplate_LandingRecovery_CompletesOnLandingBlock()
|
||||
{
|
||||
|
|
@ -135,4 +227,40 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
|
||||
Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DescendTemplate_AppliesAirBrake_WhenPlannerRequiresBrake()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: -2, max: 4);
|
||||
FlatWorldTestBuilder.ClearBox(world, -2, 79, -2, 4, 84, 2);
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, 79, 0);
|
||||
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 81, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Descend,
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.0, true, true, false, true, 12)
|
||||
};
|
||||
|
||||
var template = new DescendTemplate(segment, null);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.38, 80.56, 0.5),
|
||||
DeltaMovement = new Vec3d(0.42, -0.22, 0.0),
|
||||
OnGround = false,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(segment, null, pos, physics, world);
|
||||
var input = new MovementInput();
|
||||
template.Tick(pos, physics, input, world);
|
||||
|
||||
Assert.Equal(TransitionBrakingDecision.Brake, decision);
|
||||
Assert.True(input.Back, $"decision={decision} input(F={input.Forward},B={input.Back},S={input.Sprint})");
|
||||
Assert.False(input.Forward, $"decision={decision} input(F={input.Forward},B={input.Back},S={input.Sprint})");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,40 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Pathing.Execution.Templates;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class LivePathingRegressionTests
|
||||
{
|
||||
[Fact]
|
||||
public void AStar_ThreeByOneRejectionLayout_WithInvalidGoalBlock_RejectsBeforeExecution()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 135, max: 148);
|
||||
FlatWorldTestBuilder.ClearBox(world, 140, 80, 135, 148, 85, 140);
|
||||
// Match the live harness: the raised block is reachable, but the requested goal block is not standable.
|
||||
FlatWorldTestBuilder.SetSolid(world, 143, 80, 138);
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new AStarPathFinder();
|
||||
|
||||
PathResult result = finder.Calculate(
|
||||
ctx,
|
||||
startX: 141,
|
||||
startY: 80,
|
||||
startZ: 138,
|
||||
new GoalBlock(144, 81, 138),
|
||||
CancellationToken.None,
|
||||
timeoutMs: 2000);
|
||||
|
||||
Assert.Equal(PathStatus.Failed, result.Status);
|
||||
Assert.Empty(PathSegmentBuilder.FromPath(result.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SprintJumpTemplate_LandingRecoveryIntoTurn_CompletesInsideLandingBlock()
|
||||
{
|
||||
|
|
@ -24,14 +51,16 @@ public sealed class LivePathingRegressionTests
|
|||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(122.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.LandingRecovery
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
|
||||
};
|
||||
var next = new PathSegment
|
||||
{
|
||||
Start = new Location(122.5, 80, 110.5),
|
||||
End = new Location(122.5, 80, 111.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.03, true, true, false, false, 12)
|
||||
};
|
||||
|
||||
var template = new SprintJumpTemplate(segment, next);
|
||||
|
|
@ -41,5 +70,7 @@ public sealed class LivePathingRegressionTests
|
|||
|
||||
Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End), $"finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
double horizontalSpeed = Math.Sqrt(physics.DeltaMovement.X * physics.DeltaMovement.X + physics.DeltaMovement.Z * physics.DeltaMovement.Z);
|
||||
Assert.InRange(horizontalSpeed, 0.0, 0.04);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,13 @@ public sealed class PathExecutorCompletionTests
|
|||
Pitch = 0f,
|
||||
OnGround = true
|
||||
};
|
||||
var input = new MovementInput();
|
||||
var input = new MovementInput
|
||||
{
|
||||
Forward = true,
|
||||
Sprint = true,
|
||||
Jump = true,
|
||||
Back = true
|
||||
};
|
||||
var pos = new Location(1.48, 80, 0.5);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
|
||||
|
|
@ -39,4 +45,87 @@ public sealed class PathExecutorCompletionTests
|
|||
Assert.False(input.Jump);
|
||||
Assert.False(input.Back);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_CompletesStraightThreeSegmentFlatPath()
|
||||
{
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(BuildNodes(
|
||||
(100, 80, 100, MoveType.Traverse),
|
||||
(101, 80, 100, MoveType.Traverse),
|
||||
(102, 80, 100, MoveType.Traverse),
|
||||
(103, 80, 100, MoveType.Traverse)));
|
||||
|
||||
var executor = new PathExecutor(segments);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segments[0].Start, yaw: 270f);
|
||||
var input = new MovementInput();
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
|
||||
|
||||
PathExecutorState state = PathExecutorState.InProgress;
|
||||
for (int tick = 0; tick < 260; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
state = executor.Tick(pos, physics, input, world);
|
||||
if (state != PathExecutorState.InProgress)
|
||||
break;
|
||||
|
||||
physics.ApplyInput(input);
|
||||
physics.Tick(world);
|
||||
}
|
||||
|
||||
Assert.Equal(PathExecutorState.Complete, state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_ShortAcceptedPath_FromLiveSegmentZeroDriftState_CompletesWithoutFailure()
|
||||
{
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(BuildNodes(
|
||||
(100, 80, 100, MoveType.Traverse),
|
||||
(101, 80, 100, MoveType.Traverse),
|
||||
(102, 80, 100, MoveType.Traverse),
|
||||
(103, 80, 100, MoveType.Traverse)));
|
||||
|
||||
var debugLogs = new List<string>();
|
||||
var executor = new PathExecutor(segments, debugLogs.Add);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(101.56, 80.00, 100.74),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f,
|
||||
Pitch = 0f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
|
||||
|
||||
PathExecutorState state = PathExecutorState.InProgress;
|
||||
for (int tick = 0; tick < 220; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
state = executor.Tick(pos, physics, input, world);
|
||||
if (state != PathExecutorState.InProgress)
|
||||
break;
|
||||
|
||||
physics.ApplyInput(input);
|
||||
physics.Tick(world);
|
||||
}
|
||||
|
||||
Assert.True(state == PathExecutorState.Complete, $"state={state}\n{string.Join('\n', debugLogs)}");
|
||||
}
|
||||
|
||||
private static List<PathNode> BuildNodes(params (int x, int y, int z, MoveType moveUsed)[] raw)
|
||||
{
|
||||
var result = new List<PathNode>(raw.Length);
|
||||
for (int i = 0; i < raw.Length; i++)
|
||||
{
|
||||
var node = new PathNode(raw[i].x, raw[i].y, raw[i].z);
|
||||
if (i > 0)
|
||||
node.MoveUsed = raw[i].moveUsed;
|
||||
result.Add(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathPlanningContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void Get_ManagerAcceptedAscendChain_LoadsExactPlannerContract()
|
||||
{
|
||||
var store = PathingContractStore.LoadFromRepositoryRoot();
|
||||
|
||||
PathingPlannerContract contract = store.GetPlanner("manager-accepted-ascend-chain");
|
||||
|
||||
Assert.Equal(PathStatus.Success, contract.ExpectedStatus);
|
||||
Assert.Equal(6, contract.Segments.Length);
|
||||
|
||||
PathingPlannerSegmentContract firstSegment = contract.Segments[0];
|
||||
Assert.Equal(MoveType.Diagonal, firstSegment.Move);
|
||||
Assert.Equal(new PathingBlock(171, 80, 160), firstSegment.From);
|
||||
Assert.Equal(new PathingBlock(172, 80, 161), firstSegment.To);
|
||||
|
||||
PathingPlannerSegmentContract lastSegment = contract.Segments[5];
|
||||
Assert.Equal(MoveType.Ascend, lastSegment.Move);
|
||||
Assert.Equal(new PathingBlock(176, 82, 162), lastSegment.From);
|
||||
Assert.Equal(new PathingBlock(177, 83, 162), lastSegment.To);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using MinecraftClient.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathSegmentManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Tick_AcceptedAscendChain_FromSterileStart_CompletesWithoutReplan()
|
||||
{
|
||||
var debugLogs = new List<string>();
|
||||
var infoLogs = new List<string>();
|
||||
var manager = new PathSegmentManager(debugLog: debugLogs.Add, infoLog: infoLogs.Add);
|
||||
var goal = new GoalBlock(177, 83, 162);
|
||||
var path = BuildNodes(
|
||||
(171, 80, 160, MoveType.Traverse),
|
||||
(172, 80, 161, MoveType.Diagonal),
|
||||
(173, 80, 162, MoveType.Diagonal),
|
||||
(174, 80, 162, MoveType.Traverse),
|
||||
(175, 81, 162, MoveType.Ascend),
|
||||
(176, 82, 162, MoveType.Ascend),
|
||||
(177, 83, 162, MoveType.Ascend));
|
||||
var result = new PathResult(PathStatus.Success, path, nodesExplored: 7, elapsedMs: 1);
|
||||
|
||||
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);
|
||||
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(new Location(171.5, 80, 160.5), yaw: 315f);
|
||||
var input = new MovementInput();
|
||||
var recentTrace = new Queue<string>();
|
||||
|
||||
manager.StartNavigation(goal, result);
|
||||
|
||||
for (int tick = 0; tick < 420 && manager.IsNavigating; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
manager.Tick(pos, physics, input, world);
|
||||
recentTrace.Enqueue(
|
||||
$"tick={tick} pos={pos} vel={physics.DeltaMovement} onGround={physics.OnGround} yaw={physics.Yaw:F1} input(F={input.Forward},B={input.Back},J={input.Jump},S={input.Sprint})");
|
||||
if (recentTrace.Count > 40)
|
||||
recentTrace.Dequeue();
|
||||
if (!manager.IsNavigating)
|
||||
break;
|
||||
|
||||
physics.ApplyInput(input);
|
||||
physics.Tick(world);
|
||||
}
|
||||
|
||||
Assert.True(!manager.IsNavigating && manager.ReplanCount == 0,
|
||||
$"replanCount={manager.ReplanCount}\ninfo={string.Join('\n', infoLogs)}\ndebug={string.Join('\n', debugLogs)}\ntrace={string.Join('\n', recentTrace)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_AcceptedDescendStaircase_FromSterileStart_CompletesWithoutReplan()
|
||||
{
|
||||
var debugLogs = new List<string>();
|
||||
var infoLogs = new List<string>();
|
||||
var manager = new PathSegmentManager(debugLog: debugLogs.Add, infoLog: infoLogs.Add);
|
||||
var goal = new GoalBlock(367, 80, 360);
|
||||
var path = BuildNodes(
|
||||
(362, 85, 360, MoveType.Traverse),
|
||||
(364, 83, 360, MoveType.Descend),
|
||||
(366, 81, 360, MoveType.Descend),
|
||||
(367, 80, 360, MoveType.Descend));
|
||||
var result = new PathResult(PathStatus.Success, path, nodesExplored: 4, elapsedMs: 1);
|
||||
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 358, max: 369);
|
||||
FlatWorldTestBuilder.ClearBox(world, 360, 79, 358, 369, 85, 362);
|
||||
FlatWorldTestBuilder.FillSolid(world, 362, 84, 359, 362, 84, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 363, 83, 359, 363, 83, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 364, 82, 359, 364, 82, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 365, 81, 359, 365, 81, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 366, 80, 359, 366, 80, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 367, 79, 359, 367, 79, 361);
|
||||
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(new Location(362.5, 85, 360.5), yaw: 270f);
|
||||
var input = new MovementInput();
|
||||
var recentTrace = new Queue<string>();
|
||||
|
||||
manager.StartNavigation(goal, result);
|
||||
|
||||
for (int tick = 0; tick < 420 && manager.IsNavigating; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
manager.Tick(pos, physics, input, world);
|
||||
recentTrace.Enqueue(
|
||||
$"tick={tick} pos={pos} vel={physics.DeltaMovement} onGround={physics.OnGround} yaw={physics.Yaw:F1} input(F={input.Forward},B={input.Back},J={input.Jump},S={input.Sprint})");
|
||||
if (recentTrace.Count > 40)
|
||||
recentTrace.Dequeue();
|
||||
if (!manager.IsNavigating)
|
||||
break;
|
||||
|
||||
physics.ApplyInput(input);
|
||||
physics.Tick(world);
|
||||
}
|
||||
|
||||
Assert.True(!manager.IsNavigating && manager.ReplanCount == 0,
|
||||
$"replanCount={manager.ReplanCount}\ninfo={string.Join('\n', infoLogs)}\ndebug={string.Join('\n', debugLogs)}\ntrace={string.Join('\n', recentTrace)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_CompletesNavigation_WhenReplanStartsInsideGoalBlock()
|
||||
{
|
||||
var infoLogs = new List<string>();
|
||||
var manager = new PathSegmentManager(infoLog: infoLogs.Add);
|
||||
var goal = new GoalBlock(1, 80, 0);
|
||||
var path = new[]
|
||||
{
|
||||
new PathNode(0, 80, 0),
|
||||
new PathNode(1, 80, 0) { MoveUsed = MoveType.Traverse }
|
||||
};
|
||||
var result = new PathResult(PathStatus.Success, path, nodesExplored: 2, elapsedMs: 1);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 0, max: 4);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.10, 80.0, 0.5),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = false,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
|
||||
manager.StartNavigation(goal, result);
|
||||
|
||||
for (int tick = 0; tick < 60 && manager.IsNavigating; tick++)
|
||||
{
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
manager.Tick(pos, physics, input, world);
|
||||
}
|
||||
|
||||
Assert.False(manager.IsNavigating);
|
||||
Assert.Null(manager.Goal);
|
||||
Assert.Equal(1, manager.ReplanCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_ShortAcceptedPath_FromSterileStart_CompletesWithoutIncrementingReplanCount()
|
||||
{
|
||||
var infoLogs = new List<string>();
|
||||
var manager = new PathSegmentManager(infoLog: infoLogs.Add);
|
||||
var goal = new GoalBlock(103, 80, 100);
|
||||
var path = new[]
|
||||
{
|
||||
new PathNode(100, 80, 100),
|
||||
new PathNode(101, 80, 100) { MoveUsed = MoveType.Traverse },
|
||||
new PathNode(102, 80, 100) { MoveUsed = MoveType.Traverse },
|
||||
new PathNode(103, 80, 100) { MoveUsed = MoveType.Traverse }
|
||||
};
|
||||
var result = new PathResult(PathStatus.Success, path, nodesExplored: 4, elapsedMs: 1);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(new Location(100.5, 80, 100.5), yaw: 270f);
|
||||
var input = new MovementInput();
|
||||
|
||||
manager.StartNavigation(goal, result);
|
||||
|
||||
for (int tick = 0; tick < 260 && 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);
|
||||
}
|
||||
|
||||
Assert.False(manager.IsNavigating);
|
||||
Assert.Equal(0, manager.ReplanCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_ShortAcceptedPath_FromLiveSegmentZeroDriftState_CompletesWithoutReplan()
|
||||
{
|
||||
var debugLogs = new List<string>();
|
||||
var infoLogs = new List<string>();
|
||||
var manager = new PathSegmentManager(debugLog: debugLogs.Add, infoLog: infoLogs.Add);
|
||||
var goal = new GoalBlock(103, 80, 100);
|
||||
var path = new[]
|
||||
{
|
||||
new PathNode(100, 80, 100),
|
||||
new PathNode(101, 80, 100) { MoveUsed = MoveType.Traverse },
|
||||
new PathNode(102, 80, 100) { MoveUsed = MoveType.Traverse },
|
||||
new PathNode(103, 80, 100) { MoveUsed = MoveType.Traverse }
|
||||
};
|
||||
var result = new PathResult(PathStatus.Success, path, nodesExplored: 4, elapsedMs: 1);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(101.56, 80.00, 100.74),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f,
|
||||
Pitch = 0f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
|
||||
manager.StartNavigation(goal, result);
|
||||
|
||||
for (int tick = 0; tick < 260 && 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);
|
||||
}
|
||||
|
||||
Assert.True(!manager.IsNavigating && manager.ReplanCount == 0,
|
||||
$"replanCount={manager.ReplanCount}\ninfo={string.Join('\n', infoLogs)}\ndebug={string.Join('\n', debugLogs)}");
|
||||
}
|
||||
|
||||
private static List<PathNode> BuildNodes(params (int x, int y, int z, MoveType moveUsed)[] raw)
|
||||
{
|
||||
var result = new List<PathNode>(raw.Length);
|
||||
for (int i = 0; i < raw.Length; i++)
|
||||
{
|
||||
var node = new PathNode(raw[i].x, raw[i].y, raw[i].z);
|
||||
if (i > 0)
|
||||
node.MoveUsed = raw[i].moveUsed;
|
||||
result.Add(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using System.Collections.Generic;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathTransitionHintsTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromPath_AssignsTurnHints_WhenNextSegmentChangesHeading()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(0, 80, 0, MoveType.Traverse),
|
||||
(1, 80, 0, MoveType.Traverse),
|
||||
(1, 80, 1, MoveType.Traverse));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
PathTransitionHints hints = segments[0].ExitHints;
|
||||
|
||||
Assert.Equal(PathTransitionType.Turn, segments[0].ExitTransition);
|
||||
Assert.True(hints.RequireStableFooting);
|
||||
Assert.True(hints.RequireGrounded);
|
||||
Assert.Equal(0, hints.DesiredHeadingX);
|
||||
Assert.Equal(1, hints.DesiredHeadingZ);
|
||||
Assert.InRange(hints.MaxExitSpeed, 0.0, 0.05);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPath_AssignsJumpReadyHints_WhenNextSegmentIsParkour()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(120, 80, 110, MoveType.Traverse),
|
||||
(121, 80, 110, MoveType.Traverse),
|
||||
(123, 80, 110, MoveType.Parkour));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
PathTransitionHints hints = segments[0].ExitHints;
|
||||
|
||||
Assert.Equal(PathTransitionType.PrepareJump, segments[0].ExitTransition);
|
||||
Assert.True(hints.RequireJumpReady);
|
||||
Assert.False(hints.RequireStableFooting);
|
||||
Assert.Equal(1, hints.DesiredHeadingX);
|
||||
Assert.Equal(0, hints.DesiredHeadingZ);
|
||||
Assert.True(hints.MinExitSpeed >= 0.10, $"MinExitSpeed={hints.MinExitSpeed}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPath_AssignsZeroRunUpSpeedHints_WhenNextSegmentIsAscend()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(174, 80, 162, MoveType.Traverse),
|
||||
(175, 80, 162, MoveType.Traverse),
|
||||
(176, 81, 162, MoveType.Ascend));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
PathTransitionHints hints = segments[0].ExitHints;
|
||||
|
||||
Assert.Equal(PathTransitionType.PrepareJump, segments[0].ExitTransition);
|
||||
Assert.True(hints.RequireJumpReady);
|
||||
Assert.Equal(0.0, hints.MinExitSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPath_AssignsPreciseStopHints_WhenSegmentIsFinalStop()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(10, 80, 10, MoveType.Traverse),
|
||||
(11, 80, 10, MoveType.Traverse));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
PathTransitionHints hints = segments[0].ExitHints;
|
||||
|
||||
Assert.Equal(PathTransitionType.FinalStop, segments[0].ExitTransition);
|
||||
Assert.True(hints.RequireStableFooting);
|
||||
Assert.True(hints.RequireGrounded);
|
||||
Assert.InRange(hints.MaxExitSpeed, 0.0, 0.02);
|
||||
}
|
||||
|
||||
private static List<PathNode> BuildNodes(params (int x, int y, int z, MoveType moveUsed)[] raw)
|
||||
{
|
||||
var result = new List<PathNode>(raw.Length);
|
||||
for (int i = 0; i < raw.Length; i++)
|
||||
{
|
||||
var node = new PathNode(raw[i].x, raw[i].y, raw[i].z);
|
||||
if (i > 0)
|
||||
node.MoveUsed = raw[i].moveUsed;
|
||||
result.Add(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Pathing.Execution.Templates;
|
||||
using MinecraftClient.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
|
@ -59,7 +61,7 @@ public sealed class SprintJumpTemplateScenarioTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SprintJumpTemplate_TwoBlockGap_LandingRecovery_CompletesInsideLandingBlock()
|
||||
public void SprintJumpTemplate_TwoBlockGap_LandingRecovery_CompletesOnTurnEntrySupportStrip()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 0, max: 16);
|
||||
FlatWorldTestBuilder.ClearBox(world, 0, 79, 0, 4, 82, 2);
|
||||
|
|
@ -90,6 +92,75 @@ public sealed class SprintJumpTemplateScenarioTests
|
|||
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 140, out Location finalPos);
|
||||
|
||||
Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End));
|
||||
Assert.True(
|
||||
TemplateFootingHelper.IsCenterInsideSupportStrip(finalPos, segment.End, next.End),
|
||||
$"finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SprintJumpTemplate_LandingRecoveryIntoTurn_CompletesWithLowResidualSpeed()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
|
||||
FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112);
|
||||
FlatWorldTestBuilder.SetSolid(world, 120, 79, 110);
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 110);
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 111);
|
||||
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(123.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
|
||||
};
|
||||
var next = new PathSegment
|
||||
{
|
||||
Start = new Location(123.5, 80, 110.5),
|
||||
End = new Location(123.5, 80, 111.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.03, true, true, false, false, 12)
|
||||
};
|
||||
|
||||
var template = new SprintJumpTemplate(segment, next);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 270f);
|
||||
|
||||
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 160, out Location finalPos);
|
||||
double horizontalSpeed = Math.Sqrt(physics.DeltaMovement.X * physics.DeltaMovement.X + physics.DeltaMovement.Z * physics.DeltaMovement.Z);
|
||||
|
||||
Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
Assert.True(
|
||||
TemplateFootingHelper.IsCenterInsideSupportStrip(finalPos, segment.End, next.End),
|
||||
$"finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
Assert.InRange(horizontalSpeed, 0.0, 0.04);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SprintJumpTemplate_ThreeBlockGap_WithIsolatedTakeoffBlock_JumpsImmediately()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
|
||||
FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112);
|
||||
FlatWorldTestBuilder.SetSolid(world, 120, 79, 110);
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 110);
|
||||
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(123.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
};
|
||||
|
||||
var template = new SprintJumpTemplate(segment, null);
|
||||
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 270f);
|
||||
var input = new MovementInput();
|
||||
|
||||
TemplateState state = template.Tick(segment.Start, physics, input, world);
|
||||
|
||||
Assert.Equal(TemplateState.InProgress, state);
|
||||
Assert.True(input.Forward);
|
||||
Assert.True(input.Sprint);
|
||||
Assert.True(input.Jump);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,4 +44,55 @@ public sealed class TemplateFootingTests
|
|||
|
||||
Assert.True(exitsNextTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCenterInsideTargetBlock_ReturnsTrue_WhenPlayerStopsNearEdge()
|
||||
{
|
||||
bool inside = TemplateFootingHelper.IsCenterInsideTargetBlock(
|
||||
new Location(10.29, 80.0, 4.50),
|
||||
new Location(10.50, 80.0, 4.50));
|
||||
|
||||
Assert.True(inside);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsFootprintInsideSupportStrip_ReturnsTrue_WhenPlayerStraddlesTurnEntryBlocks()
|
||||
{
|
||||
bool inside = TemplateFootingHelper.IsFootprintInsideSupportStrip(
|
||||
new Location(123.46, 80.0, 110.77),
|
||||
new Location(123.50, 80.0, 110.50),
|
||||
new Location(123.50, 80.0, 111.50));
|
||||
|
||||
Assert.True(inside);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillLeaveSupportStripNextTick_ReturnsFalse_WhenLowSpeedStaysOnTurnEntryBlocks()
|
||||
{
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(123.46, 80.0, 110.77),
|
||||
DeltaMovement = new Vec3d(-0.0199, 0.0, 0.0040),
|
||||
OnGround = true
|
||||
};
|
||||
|
||||
bool exitsNextTick = TemplateFootingHelper.WillLeaveSupportStripNextTick(
|
||||
new Location(123.46, 80.0, 110.77),
|
||||
physics,
|
||||
new Location(123.50, 80.0, 110.50),
|
||||
new Location(123.50, 80.0, 111.50));
|
||||
|
||||
Assert.False(exitsNextTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCenterInsideSupportStrip_ReturnsTrue_WhenLowSpeedTurnEntryStopsOnSeam()
|
||||
{
|
||||
bool inside = TemplateFootingHelper.IsCenterInsideSupportStrip(
|
||||
new Location(123.46, 80.0, 110.77),
|
||||
new Location(123.50, 80.0, 110.50),
|
||||
new Location(123.50, 80.0, 111.50));
|
||||
|
||||
Assert.True(inside);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public sealed class TransitionBrakingPlannerTests
|
|||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.ContinueStraight,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.0, double.PositiveInfinity, false, false, false, false, 8),
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
|
|
@ -29,6 +30,50 @@ public sealed class TransitionBrakingPlannerTests
|
|||
Assert.False(decision.HoldBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_Brakes_ForTurnEntryRequiringSlowSpeed()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var physics = CreatePhysics(0.156, 0.0, onGround: true);
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.Turn,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12),
|
||||
PreserveSprint = false
|
||||
};
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(current, null, new Location(1.38, 80, 0.5), physics, world);
|
||||
|
||||
Assert.False(decision.HoldForward);
|
||||
Assert.False(decision.HoldSprint);
|
||||
Assert.True(decision.HoldBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_Carries_ForPrepareJumpNeedingRunUpSpeed()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var physics = CreatePhysics(0.0, 0.0, onGround: true);
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.PrepareJump,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.12, double.PositiveInfinity, false, true, true, false, 10),
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(current, null, new Location(1.02, 80, 0.5), physics, world);
|
||||
|
||||
Assert.True(decision.HoldForward);
|
||||
Assert.True(decision.HoldSprint);
|
||||
Assert.False(decision.HoldBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_BackBrakes_ForFinalStop_WhenRemainingRunwayIsTooShort()
|
||||
{
|
||||
|
|
@ -40,6 +85,7 @@ public sealed class TransitionBrakingPlannerTests
|
|||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.03, true, true, false, false, 12),
|
||||
PreserveSprint = false
|
||||
};
|
||||
|
||||
|
|
@ -61,6 +107,7 @@ public sealed class TransitionBrakingPlannerTests
|
|||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.03, true, true, false, false, 12),
|
||||
PreserveSprint = false
|
||||
};
|
||||
|
||||
|
|
@ -81,6 +128,7 @@ public sealed class TransitionBrakingPlannerTests
|
|||
End = new Location(123.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.Turn,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12),
|
||||
PreserveSprint = false
|
||||
};
|
||||
var next = new PathSegment
|
||||
|
|
@ -91,7 +139,9 @@ public sealed class TransitionBrakingPlannerTests
|
|||
ExitTransition = PathTransitionType.FinalStop
|
||||
};
|
||||
|
||||
bool release = TransitionBrakingPlanner.ShouldReleaseForwardInAir(current, next, new Location(123.18, 80.92, 110.5), physics);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
|
||||
|
||||
bool release = TransitionBrakingPlanner.ShouldReleaseForwardInAir(current, next, new Location(123.18, 80.92, 110.5), physics, world);
|
||||
|
||||
Assert.True(release);
|
||||
}
|
||||
|
|
@ -112,6 +162,7 @@ public sealed class TransitionBrakingPlannerTests
|
|||
End = new Location(122.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12),
|
||||
PreserveSprint = false
|
||||
};
|
||||
var next = new PathSegment
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class TransitionLookaheadEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ChooseGroundProfile_PicksBrake_WhenTurnEntryCapsResidualSpeed()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.Turn,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
|
||||
};
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.34, 80.0, 0.5),
|
||||
DeltaMovement = new Vec3d(0.156, 0.0, 0.0),
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseGroundProfile(
|
||||
current,
|
||||
new Location(1.34, 80.0, 0.5),
|
||||
physics,
|
||||
world);
|
||||
|
||||
Assert.Equal(TransitionInputProfile.Brake, profile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseGroundProfile_PicksCarry_WhenPrepareJumpNeedsRunUpSpeed()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.PrepareJump,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.12, double.PositiveInfinity, false, true, true, false, 10),
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.02, 80.0, 0.5),
|
||||
DeltaMovement = new Vec3d(0.086, 0.0, 0.0),
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseGroundProfile(
|
||||
current,
|
||||
new Location(1.02, 80.0, 0.5),
|
||||
physics,
|
||||
world);
|
||||
|
||||
Assert.Equal(TransitionInputProfile.Carry, profile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseAirProfile_PicksRelease_WhenLandingNeedsSlowStableEntry()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(123.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
|
||||
};
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(123.06, 80.92, 110.5),
|
||||
DeltaMovement = new Vec3d(0.31, 0.0, 0.0),
|
||||
OnGround = false,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseAirProfile(
|
||||
current,
|
||||
new Location(123.06, 80.92, 110.5),
|
||||
physics,
|
||||
world);
|
||||
|
||||
Assert.Equal(TransitionInputProfile.AirRelease, profile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseAirProfile_KeepsForward_WhenThreeBlockLandingRecoveryIsStillShort()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
|
||||
FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112);
|
||||
FlatWorldTestBuilder.SetSolid(world, 120, 79, 110);
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 110);
|
||||
FlatWorldTestBuilder.SetSolid(world, 123, 79, 111);
|
||||
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(120.5, 80, 110.5),
|
||||
End = new Location(123.5, 80, 110.5),
|
||||
MoveType = MoveType.Parkour,
|
||||
ExitTransition = PathTransitionType.LandingRecovery,
|
||||
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
|
||||
};
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(122.13, 81.02, 110.5),
|
||||
DeltaMovement = new Vec3d(0.1798, -0.2277, 0.0),
|
||||
OnGround = false,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseAirProfile(
|
||||
current,
|
||||
new Location(122.13, 81.02, 110.5),
|
||||
physics,
|
||||
world);
|
||||
|
||||
Assert.Equal(TransitionInputProfile.AirHoldForward, profile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseGroundProfile_PicksCarry_WhenFinalDescendHasNotClearedUpperSupport()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 360, max: 369);
|
||||
FlatWorldTestBuilder.ClearBox(world, 360, 79, 358, 369, 85, 362);
|
||||
FlatWorldTestBuilder.FillSolid(world, 362, 84, 359, 362, 84, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 363, 83, 359, 363, 83, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 364, 82, 359, 364, 82, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 365, 81, 359, 365, 81, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 366, 80, 359, 366, 80, 361);
|
||||
FlatWorldTestBuilder.FillSolid(world, 367, 79, 359, 367, 79, 361);
|
||||
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(366.5, 81, 360.5),
|
||||
End = new Location(367.5, 80, 360.5),
|
||||
MoveType = MoveType.Descend,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.02, true, true, false, false, 12)
|
||||
};
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(367.2316, 81.0, 360.4698),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseGroundProfile(
|
||||
current,
|
||||
new Location(367.2316, 81.0, 360.4698),
|
||||
physics,
|
||||
world);
|
||||
|
||||
Assert.Equal(TransitionInputProfile.Carry, profile);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"manager-accepted-ascend-chain": {
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"move": "Diagonal",
|
||||
"from": { "x": 171, "y": 80, "z": 160 },
|
||||
"to": { "x": 172, "y": 80, "z": 161 }
|
||||
},
|
||||
{
|
||||
"move": "Diagonal",
|
||||
"from": { "x": 172, "y": 80, "z": 161 },
|
||||
"to": { "x": 173, "y": 80, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Traverse",
|
||||
"from": { "x": 173, "y": 80, "z": 162 },
|
||||
"to": { "x": 174, "y": 80, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 174, "y": 80, "z": 162 },
|
||||
"to": { "x": 175, "y": 81, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 175, "y": 81, "z": 162 },
|
||||
"to": { "x": 176, "y": 82, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 176, "y": 82, "z": 162 },
|
||||
"to": { "x": 177, "y": 83, "z": 162 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"manager-accepted-ascend-chain": {
|
||||
"totalBudgetMs": 0,
|
||||
"segments": [
|
||||
{ "move": "Diagonal", "budgetMs": 0 },
|
||||
{ "move": "Diagonal", "budgetMs": 0 },
|
||||
{ "move": "Traverse", "budgetMs": 0 },
|
||||
{ "move": "Ascend", "budgetMs": 0 },
|
||||
{ "move": "Ascend", "budgetMs": 0 },
|
||||
{ "move": "Ascend", "budgetMs": 0 }
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue