mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add transition-aware path execution braking
This commit is contained in:
parent
945eae958a
commit
3b4e552d70
23 changed files with 837 additions and 108 deletions
25
MinecraftClient.Tests/MinecraftClient.Tests.csproj
Normal file
25
MinecraftClient.Tests/MinecraftClient.Tests.csproj
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MinecraftClient\MinecraftClient.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
internal static class FlatWorldTestBuilder
|
||||
{
|
||||
private static readonly Lock InitLock = new();
|
||||
private static bool _defaultsLoaded;
|
||||
|
||||
public static World CreateStoneFloor(int floorY = 79, int min = -32, int max = 32)
|
||||
{
|
||||
EnsureDefaultDimensionsLoaded();
|
||||
World.SetDimension("minecraft:overworld");
|
||||
|
||||
var world = new World();
|
||||
int minChunk = (int)Math.Floor(min / 16.0);
|
||||
int maxChunk = (int)Math.Floor(max / 16.0);
|
||||
|
||||
for (int chunkX = minChunk; chunkX <= maxChunk; chunkX++)
|
||||
{
|
||||
for (int chunkZ = minChunk; chunkZ <= maxChunk; chunkZ++)
|
||||
{
|
||||
world[chunkX, chunkZ] = new ChunkColumn(24) { FullyLoaded = true };
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = min; x <= max; x++)
|
||||
{
|
||||
for (int z = min; z <= max; z++)
|
||||
{
|
||||
world.SetBlock(new Location(x, floorY, z), new Block(1));
|
||||
}
|
||||
}
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
private static void EnsureDefaultDimensionsLoaded()
|
||||
{
|
||||
lock (InitLock)
|
||||
{
|
||||
if (_defaultsLoaded)
|
||||
return;
|
||||
|
||||
World.LoadDefaultDimensions1206Plus();
|
||||
_defaultsLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathExecutorCompletionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Tick_ClearsMovementInput_WhenSegmentCompletes()
|
||||
{
|
||||
var executor = new PathExecutor(new List<PathSegment>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse
|
||||
}
|
||||
});
|
||||
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Yaw = 270f,
|
||||
Pitch = 0f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
var pos = new Location(1.48, 80, 0.5);
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
|
||||
var state = executor.Tick(pos, physics, input, world);
|
||||
|
||||
Assert.Equal(PathExecutorState.Complete, state);
|
||||
Assert.False(input.Forward);
|
||||
Assert.False(input.Sprint);
|
||||
Assert.False(input.Jump);
|
||||
Assert.False(input.Back);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class PathSegmentBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromPath_AnnotatesStraightTraverse_AsContinueStraight()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(0, 80, 0, MoveType.Traverse),
|
||||
(1, 80, 0, MoveType.Traverse),
|
||||
(2, 80, 0, MoveType.Traverse));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
|
||||
Assert.Equal(PathTransitionType.ContinueStraight, segments[0].ExitTransition);
|
||||
Assert.True(segments[0].PreserveSprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPath_AnnotatesOrthogonalTraverse_AsTurn()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(0, 80, 0, MoveType.Traverse),
|
||||
(1, 80, 0, MoveType.Traverse),
|
||||
(1, 80, 1, MoveType.Traverse));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
|
||||
Assert.Equal(PathTransitionType.Turn, segments[0].ExitTransition);
|
||||
Assert.False(segments[0].PreserveSprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPath_AnnotatesTraverseIntoParkour_AsPrepareJump()
|
||||
{
|
||||
var nodes = BuildNodes(
|
||||
(120, 80, 110, MoveType.Traverse),
|
||||
(121, 80, 110, MoveType.Traverse),
|
||||
(123, 80, 110, MoveType.Parkour));
|
||||
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
|
||||
|
||||
Assert.Equal(PathTransitionType.PrepareJump, segments[0].ExitTransition);
|
||||
Assert.True(segments[0].PreserveSprint);
|
||||
}
|
||||
|
||||
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,79 @@
|
|||
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;
|
||||
|
||||
public sealed class TemplateBrakingTests
|
||||
{
|
||||
[Fact]
|
||||
public void WalkTemplate_BackBrakes_WhenFinalStopIsTooClose()
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var segment = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(1.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
PreserveSprint = false
|
||||
};
|
||||
|
||||
var template = new WalkTemplate(segment, null);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.38, 80.0, 0.5),
|
||||
DeltaMovement = new Vec3d(0.156, 0.0, 0.0),
|
||||
OnGround = true,
|
||||
Yaw = 270f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
|
||||
TemplateState state = template.Tick(new Location(1.38, 80, 0.5), physics, input, world);
|
||||
|
||||
Assert.Equal(TemplateState.InProgress, state);
|
||||
Assert.False(input.Forward);
|
||||
Assert.False(input.Sprint);
|
||||
Assert.True(input.Back);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTemplate_KeepsForward_WhenTransitionContinuesStraight()
|
||||
{
|
||||
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.ContinueStraight,
|
||||
PreserveSprint = true
|
||||
};
|
||||
var next = new PathSegment
|
||||
{
|
||||
Start = new Location(1.5, 80, 0.5),
|
||||
End = new Location(2.5, 80, 0.5),
|
||||
MoveType = MoveType.Traverse,
|
||||
ExitTransition = PathTransitionType.FinalStop
|
||||
};
|
||||
|
||||
var template = new WalkTemplate(current, next);
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(1.10, 80.0, 0.5),
|
||||
DeltaMovement = new Vec3d(0.140, 0.0, 0.0),
|
||||
OnGround = true,
|
||||
Yaw = 270f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
|
||||
TemplateState state = template.Tick(new Location(1.10, 80, 0.5), physics, input, world);
|
||||
|
||||
Assert.Equal(TemplateState.InProgress, state);
|
||||
Assert.True(input.Forward);
|
||||
Assert.True(input.Sprint);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using MinecraftClient.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution;
|
||||
|
||||
public sealed class TransitionBrakingPlannerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Plan_ReturnsCarryMomentum_ForContinueStraight()
|
||||
{
|
||||
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.ContinueStraight,
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(current, null, new Location(1.05, 80, 0.5), physics, world);
|
||||
|
||||
Assert.True(decision.HoldForward);
|
||||
Assert.True(decision.HoldSprint);
|
||||
Assert.False(decision.HoldBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_BackBrakes_ForFinalStop_WhenRemainingRunwayIsTooShort()
|
||||
{
|
||||
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.FinalStop,
|
||||
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_NudgesForward_ForFinalStop_WhenAlreadySlowButStillShort()
|
||||
{
|
||||
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.FinalStop,
|
||||
PreserveSprint = false
|
||||
};
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(current, null, new Location(1.41, 80, 0.5), physics, world);
|
||||
|
||||
Assert.True(decision.HoldForward);
|
||||
Assert.False(decision.HoldSprint);
|
||||
Assert.False(decision.HoldBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReleaseForwardInAir_ReturnsTrue_ForParkourIntoTurn()
|
||||
{
|
||||
var physics = CreatePhysics(0.32, 0.0, onGround: false);
|
||||
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.Turn,
|
||||
PreserveSprint = false
|
||||
};
|
||||
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
|
||||
};
|
||||
|
||||
bool release = TransitionBrakingPlanner.ShouldReleaseForwardInAir(current, next, new Location(123.18, 80.92, 110.5), physics);
|
||||
|
||||
Assert.True(release);
|
||||
}
|
||||
|
||||
private static PlayerPhysics CreatePhysics(double deltaX, double deltaZ, bool onGround)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(0.0, 80.0, 0.0),
|
||||
DeltaMovement = new Vec3d(deltaX, 0.0, deltaZ),
|
||||
OnGround = onGround,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 270f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MccMcpStdioHarness", "Debug
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MccMcpWebPlayground", "DebugTools\MccMcpWebPlayground\MccMcpWebPlayground.csproj", "{5F620CF6-BC7D-449A-B779-2D51985059C6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftClient.Tests", "MinecraftClient.Tests\MinecraftClient.Tests.csproj", "{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -71,6 +73,18 @@ Global
|
|||
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A6F319D6-4D0E-4D46-A31E-EF64E5F9F596}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ namespace MinecraftClient.Pathing.Execution
|
|||
/// </summary>
|
||||
public static class ActionTemplateFactory
|
||||
{
|
||||
public static IActionTemplate Create(PathSegment segment)
|
||||
public static IActionTemplate Create(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
return segment.MoveType switch
|
||||
{
|
||||
MoveType.Traverse => new WalkTemplate(segment.Start, segment.End),
|
||||
MoveType.Diagonal => new WalkTemplate(segment.Start, segment.End),
|
||||
MoveType.Ascend => new AscendTemplate(segment.Start, segment.End),
|
||||
MoveType.Descend => new DescendTemplate(segment.Start, segment.End),
|
||||
MoveType.Fall => new FallTemplate(segment.Start, segment.End),
|
||||
MoveType.Climb => new ClimbTemplate(segment.Start, segment.End),
|
||||
MoveType.Parkour => new SprintJumpTemplate(segment.Start, segment.End),
|
||||
MoveType.Traverse => new WalkTemplate(segment, nextSegment),
|
||||
MoveType.Diagonal => new WalkTemplate(segment, nextSegment),
|
||||
MoveType.Ascend => new AscendTemplate(segment, nextSegment),
|
||||
MoveType.Descend => new DescendTemplate(segment, nextSegment),
|
||||
MoveType.Fall => new FallTemplate(segment, nextSegment),
|
||||
MoveType.Climb => new ClimbTemplate(segment, nextSegment),
|
||||
MoveType.Parkour => new SprintJumpTemplate(segment, nextSegment),
|
||||
_ => throw new ArgumentException($"Unknown MoveType: {segment.MoveType}")
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ namespace MinecraftClient.Pathing.Execution
|
|||
Location ExpectedStart { get; }
|
||||
Location ExpectedEnd { get; }
|
||||
|
||||
TemplateState Tick(Location currentPos, PlayerPhysics physics, MovementInput input);
|
||||
TemplateState Tick(Location currentPos, PlayerPhysics physics, MovementInput input, World world);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,16 +37,20 @@ namespace MinecraftClient.Pathing.Execution
|
|||
AdvanceToNextSegment();
|
||||
}
|
||||
|
||||
public PathExecutorState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public PathExecutorState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
if (_currentTemplate is null)
|
||||
{
|
||||
input.Reset();
|
||||
return PathExecutorState.Complete;
|
||||
}
|
||||
|
||||
var state = _currentTemplate.Tick(pos, physics, input);
|
||||
var state = _currentTemplate.Tick(pos, physics, input, world);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case TemplateState.Complete:
|
||||
input.Reset();
|
||||
_debugLog?.Invoke($"[PathExec] Segment {_currentIndex} complete " +
|
||||
$"({_segments[_currentIndex].MoveType}) at ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
|
||||
_currentIndex++;
|
||||
|
|
@ -60,6 +64,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
return PathExecutorState.InProgress;
|
||||
|
||||
case TemplateState.Failed:
|
||||
input.Reset();
|
||||
_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})");
|
||||
|
|
@ -75,7 +80,8 @@ namespace MinecraftClient.Pathing.Execution
|
|||
if (_currentIndex < _segments.Count)
|
||||
{
|
||||
var seg = _segments[_currentIndex];
|
||||
_currentTemplate = ActionTemplateFactory.Create(seg);
|
||||
PathSegment? next = _currentIndex + 1 < _segments.Count ? _segments[_currentIndex + 1] : null;
|
||||
_currentTemplate = ActionTemplateFactory.Create(seg, next);
|
||||
_debugLog?.Invoke($"[PathExec] Starting segment {_currentIndex}/{_segments.Count}: {seg}");
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
|
|
@ -9,25 +9,13 @@ namespace MinecraftClient.Pathing.Execution
|
|||
public required Location Start { get; init; }
|
||||
public required Location End { get; init; }
|
||||
public required MoveType MoveType { get; init; }
|
||||
public PathTransitionType ExitTransition { get; init; } = PathTransitionType.FinalStop;
|
||||
public bool PreserveSprint { get; init; }
|
||||
|
||||
public static List<PathSegment> FromPath(IReadOnlyList<PathNode> nodes)
|
||||
{
|
||||
var segments = new List<PathSegment>(nodes.Count - 1);
|
||||
for (int i = 1; i < nodes.Count; i++)
|
||||
{
|
||||
var prev = nodes[i - 1];
|
||||
var curr = nodes[i];
|
||||
segments.Add(new PathSegment
|
||||
{
|
||||
Start = new Location(prev.X + 0.5, prev.Y, prev.Z + 0.5),
|
||||
End = new Location(curr.X + 0.5, curr.Y, curr.Z + 0.5),
|
||||
MoveType = curr.MoveUsed
|
||||
});
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
public int HeadingX => Math.Sign(End.X - Start.X);
|
||||
public int HeadingZ => Math.Sign(End.Z - Start.Z);
|
||||
|
||||
public override string ToString() =>
|
||||
$"{MoveType}: ({Start.X:F1},{Start.Y:F1},{Start.Z:F1})->({End.X:F1},{End.Y:F1},{End.Z:F1})";
|
||||
$"{MoveType}: ({Start.X:F1},{Start.Y:F1},{Start.Z:F1})->({End.X:F1},{End.Y:F1},{End.Z:F1}), transition={ExitTransition}, preserveSprint={PreserveSprint}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
MinecraftClient/Pathing/Execution/PathSegmentBuilder.cs
Normal file
67
MinecraftClient/Pathing/Execution/PathSegmentBuilder.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public static class PathSegmentBuilder
|
||||
{
|
||||
public static List<PathSegment> FromPath(IReadOnlyList<PathNode> nodes)
|
||||
{
|
||||
var segments = new List<PathSegment>(Math.Max(0, nodes.Count - 1));
|
||||
for (int i = 1; i < nodes.Count; i++)
|
||||
{
|
||||
PathSegment? next = null;
|
||||
if (i + 1 < nodes.Count)
|
||||
{
|
||||
var nextNode = nodes[i + 1];
|
||||
var curr = nodes[i];
|
||||
next = new PathSegment
|
||||
{
|
||||
Start = new Location(curr.X + 0.5, curr.Y, curr.Z + 0.5),
|
||||
End = new Location(nextNode.X + 0.5, nextNode.Y, nextNode.Z + 0.5),
|
||||
MoveType = nextNode.MoveUsed
|
||||
};
|
||||
}
|
||||
|
||||
var prev = nodes[i - 1];
|
||||
var currNode = nodes[i];
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(prev.X + 0.5, prev.Y, prev.Z + 0.5),
|
||||
End = new Location(currNode.X + 0.5, currNode.Y, currNode.Z + 0.5),
|
||||
MoveType = currNode.MoveUsed
|
||||
};
|
||||
|
||||
PathTransitionType exitTransition = Classify(current, next);
|
||||
segments.Add(new PathSegment
|
||||
{
|
||||
Start = current.Start,
|
||||
End = current.End,
|
||||
MoveType = current.MoveType,
|
||||
ExitTransition = exitTransition,
|
||||
PreserveSprint = exitTransition is PathTransitionType.ContinueStraight or PathTransitionType.PrepareJump
|
||||
});
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static PathTransitionType Classify(PathSegment current, PathSegment? next)
|
||||
{
|
||||
if (next is null)
|
||||
return PathTransitionType.FinalStop;
|
||||
|
||||
if (next.MoveType is MoveType.Parkour or MoveType.Ascend)
|
||||
return PathTransitionType.PrepareJump;
|
||||
|
||||
if (current.MoveType is MoveType.Parkour or MoveType.Descend or MoveType.Fall)
|
||||
return PathTransitionType.LandingRecovery;
|
||||
|
||||
if (current.HeadingX == next.HeadingX && current.HeadingZ == next.HeadingZ)
|
||||
return PathTransitionType.ContinueStraight;
|
||||
|
||||
return PathTransitionType.Turn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
{
|
||||
_goal = goal;
|
||||
_replanCount = 0;
|
||||
var segments = PathSegment.FromPath(result.Path);
|
||||
var segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
_executor = new PathExecutor(segments, _debugLog);
|
||||
_infoLog?.Invoke($"[PathMgr] Navigation started: {segments.Count} segments");
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
if (_executor is null)
|
||||
return;
|
||||
|
||||
var state = _executor.Tick(pos, physics, input);
|
||||
var state = _executor.Tick(pos, physics, input, world);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
|
|
@ -113,7 +113,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
return;
|
||||
}
|
||||
|
||||
var segments = PathSegment.FromPath(result.Path);
|
||||
var segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
_executor = new PathExecutor(segments, _debugLog);
|
||||
_infoLog?.Invoke($"[PathMgr] Replanned: {segments.Count} segments (replan #{_replanCount})");
|
||||
}
|
||||
|
|
|
|||
11
MinecraftClient/Pathing/Execution/PathTransitionType.cs
Normal file
11
MinecraftClient/Pathing/Execution/PathTransitionType.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public enum PathTransitionType
|
||||
{
|
||||
FinalStop,
|
||||
ContinueStraight,
|
||||
Turn,
|
||||
PrepareJump,
|
||||
LandingRecovery
|
||||
}
|
||||
}
|
||||
|
|
@ -13,18 +13,22 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private readonly PathSegment _segment;
|
||||
private readonly PathSegment? _nextSegment;
|
||||
private int _tickCount;
|
||||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
|
||||
public AscendTemplate(Location start, Location end)
|
||||
public AscendTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
_lastPos = start;
|
||||
_segment = segment;
|
||||
_nextSegment = nextSegment;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
_lastPos = segment.Start;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
|
|
@ -43,8 +47,26 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
if (physics.OnGround && dy > 0.1)
|
||||
input.Jump = true;
|
||||
|
||||
if (horizDistSq < 0.25 && Math.Abs(dy) < 0.8)
|
||||
if (physics.OnGround && Math.Abs(dy) < 0.15)
|
||||
{
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
TemplateHelper.FaceSegmentHeading(physics, _segment);
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.ContinueStraight && horizDistSq < 0.25)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
|
||||
&& TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
}
|
||||
else if (horizDistSq < 0.25 && Math.Abs(dy) < 0.8)
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
|
||||
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);
|
||||
double movedY = Math.Abs(pos.Y - _lastPos.Y);
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private readonly bool _goingUp;
|
||||
private int _tickCount;
|
||||
|
||||
public ClimbTemplate(Location start, Location end)
|
||||
public ClimbTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
_goingUp = end.Y > start.Y;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
_goingUp = segment.End.Y > segment.Start.Y;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,20 +15,24 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private readonly PathSegment _segment;
|
||||
private readonly PathSegment? _nextSegment;
|
||||
private int _tickCount;
|
||||
private bool _hasFallen;
|
||||
private readonly bool _needsSprint;
|
||||
|
||||
public DescendTemplate(Location start, Location end)
|
||||
public DescendTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
double hdx = end.X - start.X;
|
||||
double hdz = end.Z - start.Z;
|
||||
_segment = segment;
|
||||
_nextSegment = nextSegment;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
double hdx = segment.End.X - segment.Start.X;
|
||||
double hdz = segment.End.Z - segment.Start.Z;
|
||||
_needsSprint = (hdx * hdx + hdz * hdz) > 2.25;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
|
|
@ -40,14 +44,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
if (!physics.OnGround)
|
||||
_hasFallen = true;
|
||||
|
||||
// Completion: landed on ground near destination
|
||||
if (_hasFallen && physics.OnGround && horizDistSq < 0.5 && Math.Abs(dy) < 0.8)
|
||||
return TemplateState.Complete;
|
||||
|
||||
// Completion: already at destination without falling (e.g., single step down)
|
||||
if (horizDistSq < 0.25 && Math.Abs(dy) < 0.5 && physics.OnGround)
|
||||
return TemplateState.Complete;
|
||||
|
||||
// Completion: landed in water near destination
|
||||
if (_hasFallen && physics.InWater && horizDistSq < 0.5 && Math.Abs(dy) < 2.0)
|
||||
return TemplateState.Complete;
|
||||
|
|
@ -63,7 +59,28 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
||||
if (physics.OnClimbable)
|
||||
if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 0.8 : 0.5))
|
||||
{
|
||||
if (horizDistSq > 0.01)
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
TemplateHelper.FaceSegmentHeading(physics, _segment);
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.ContinueStraight)
|
||||
{
|
||||
double completionThreshold = _hasFallen ? 0.5 : 0.25;
|
||||
if (horizDistSq < completionThreshold)
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
else if (TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
}
|
||||
else if (physics.OnClimbable)
|
||||
{
|
||||
if (horizDistSq > 0.25)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,27 +16,30 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private int _tickCount;
|
||||
private bool _hasFallen;
|
||||
|
||||
public FallTemplate(Location start, Location end)
|
||||
public FallTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = pos.Y - ExpectedEnd.Y;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
if (!physics.OnGround)
|
||||
_hasFallen = true;
|
||||
|
||||
// Solid ground landing
|
||||
if (_hasFallen && physics.OnGround && Math.Abs(dy) < 1.0)
|
||||
// Solid ground landing near the target XZ
|
||||
if (_hasFallen && physics.OnGround && Math.Abs(dy) < 1.0 && horizDistSq < 1.0)
|
||||
return TemplateState.Complete;
|
||||
|
||||
// Water landing
|
||||
if (_hasFallen && physics.InWater && Math.Abs(dy) < 2.0)
|
||||
// Water landing near the target XZ
|
||||
if (_hasFallen && physics.InWater && Math.Abs(dy) < 2.0 && horizDistSq < 1.5)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_tickCount > 200)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,17 @@ using MinecraftClient.Physics;
|
|||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprint-jump across a gap. Uses a phase-based state machine:
|
||||
/// Approach -> jump when ready -> Airborne -> Landing check.
|
||||
/// For long jumps (>= 3.5 blocks), delays the jump until the player
|
||||
/// has moved toward the edge of the starting block for maximum distance.
|
||||
/// Jump across a gap. Uses a phase-based state machine:
|
||||
/// Approach -> Jump -> Airborne -> Landing.
|
||||
///
|
||||
/// All parkour jumps use sprint-jumping (vanilla optimal horizontal distance).
|
||||
/// The key to landing on small platforms is releasing forward/sprint input mid-air
|
||||
/// once the player is close to or past the target, letting drag decelerate them
|
||||
/// onto the block.
|
||||
///
|
||||
/// During Approach, the template waits for the yaw to be within 5 degrees of
|
||||
/// the target direction before jumping. For medium/long jumps, it also builds
|
||||
/// momentum by sprinting toward the block edge.
|
||||
/// </summary>
|
||||
public sealed class SprintJumpTemplate : IActionTemplate
|
||||
{
|
||||
|
|
@ -17,22 +24,27 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private readonly PathSegment _segment;
|
||||
private readonly PathSegment? _nextSegment;
|
||||
private readonly double _horizDist;
|
||||
private readonly bool _isDiagonal;
|
||||
private int _tickCount;
|
||||
private Phase _phase = Phase.Approach;
|
||||
private bool _leftGround;
|
||||
|
||||
public SprintJumpTemplate(Location start, Location end)
|
||||
private const float YawToleranceDeg = 5f;
|
||||
|
||||
public SprintJumpTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
double dx = end.X - start.X;
|
||||
double dz = end.Z - start.Z;
|
||||
_segment = segment;
|
||||
_nextSegment = nextSegment;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
double dx = segment.End.X - segment.Start.X;
|
||||
double dz = segment.End.Z - segment.Start.Z;
|
||||
_horizDist = Math.Sqrt(dx * dx + dz * dz);
|
||||
_isDiagonal = Math.Abs(dx) > 0.5 && Math.Abs(dz) > 0.5;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
|
|
@ -45,52 +57,92 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
case Phase.Approach:
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
if (physics.OnGround)
|
||||
{
|
||||
double fromStartSq = TemplateHelper.HorizontalDistanceSq(pos, ExpectedStart);
|
||||
float yawDelta = YawDifference(physics.Yaw, targetYaw);
|
||||
|
||||
// For long jumps, delay the jump until the player has sprinted
|
||||
// toward the block edge. Baritone waits until playerFeet is in
|
||||
// the next block (~0.5 blocks from center) for dist >= 4.
|
||||
// For medium jumps (dist 3), wait 0.35 blocks (Baritone: 0.7).
|
||||
// For short diagonal jumps (<= 3 blocks), jump immediately
|
||||
// to avoid overshooting the small starting platform.
|
||||
// Build momentum before jumping. Sprint speed is ~5.6 m/s
|
||||
// (0.28 blocks/tick). More run-up = more airtime distance.
|
||||
// Standing sprint jump (0t): ~3.6 blocks horizontal
|
||||
// 2-tick sprint (0.56m): ~4.3 blocks horizontal
|
||||
// 4-tick sprint (1.1m): ~5.0 blocks horizontal
|
||||
double minApproachSq;
|
||||
if (_horizDist >= 3.5)
|
||||
minApproachSq = 0.25; // 0.5 blocks
|
||||
else if (_horizDist >= 2.5 && !_isDiagonal)
|
||||
minApproachSq = 0.12; // ~0.35 blocks
|
||||
if (_horizDist >= 5.0)
|
||||
minApproachSq = 0.64; // 0.8 blocks - 3+ ticks of sprint
|
||||
else if (_horizDist >= 4.0)
|
||||
minApproachSq = 0.36; // 0.6 blocks - 2-3 ticks of sprint
|
||||
else if (_horizDist > 2.5)
|
||||
minApproachSq = 0.09; // 0.3 blocks - 1-2 ticks of sprint
|
||||
else
|
||||
minApproachSq = 0.0;
|
||||
|
||||
if (fromStartSq >= minApproachSq)
|
||||
bool yawAligned = yawDelta < YawToleranceDeg;
|
||||
bool posReady = fromStartSq >= minApproachSq;
|
||||
|
||||
if (yawAligned && posReady)
|
||||
{
|
||||
input.Jump = true;
|
||||
_phase = Phase.Airborne;
|
||||
}
|
||||
}
|
||||
if (_tickCount > 30)
|
||||
if (_tickCount > 40)
|
||||
return TemplateState.Failed;
|
||||
break;
|
||||
|
||||
case Phase.Airborne:
|
||||
{
|
||||
if (!physics.OnGround)
|
||||
break;
|
||||
_phase = Phase.Landing;
|
||||
goto case Phase.Landing;
|
||||
_leftGround = true;
|
||||
|
||||
bool pastTarget = IsPastTarget(pos);
|
||||
bool releaseInAir = TransitionBrakingPlanner.ShouldReleaseForwardInAir(_segment, _nextSegment, pos, physics);
|
||||
|
||||
if (releaseInAir || pastTarget)
|
||||
{
|
||||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
}
|
||||
|
||||
if (_leftGround && physics.OnGround)
|
||||
{
|
||||
_phase = Phase.Landing;
|
||||
goto case Phase.Landing;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase.Landing:
|
||||
double horizTolerance = _horizDist >= 3.5 ? 3.0 : 2.0;
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
TemplateHelper.FaceSegmentHeading(physics, _segment);
|
||||
|
||||
double horizToleranceLinear = _horizDist >= 3.5 ? 1.5 : 1.0;
|
||||
double horizToleranceSq = horizToleranceLinear * horizToleranceLinear;
|
||||
double vertTolerance = Math.Abs(ExpectedEnd.Y - ExpectedStart.Y) > 0.5 ? 1.5 : 1.0;
|
||||
if (horizDistSq < horizTolerance && Math.Abs(dy) < vertTolerance)
|
||||
if (_segment.ExitTransition == PathTransitionType.ContinueStraight
|
||||
&& horizDistSq < horizToleranceSq && Math.Abs(dy) < vertTolerance)
|
||||
return TemplateState.Complete;
|
||||
return TemplateState.Failed;
|
||||
|
||||
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
|
||||
&& TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (pos.Y < ExpectedEnd.Y - 4.0)
|
||||
|
|
@ -101,5 +153,28 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
|
||||
private bool IsPastTarget(Location pos)
|
||||
{
|
||||
double dirX = ExpectedEnd.X - ExpectedStart.X;
|
||||
double dirZ = ExpectedEnd.Z - ExpectedStart.Z;
|
||||
double len = Math.Sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (len < 0.001) return false;
|
||||
dirX /= len;
|
||||
dirZ /= len;
|
||||
|
||||
double relX = pos.X - ExpectedEnd.X;
|
||||
double relZ = pos.Z - ExpectedEnd.Z;
|
||||
double dot = relX * dirX + relZ * dirZ;
|
||||
return dot > 0.0;
|
||||
}
|
||||
|
||||
private static float YawDifference(float current, float target)
|
||||
{
|
||||
float delta = target - current;
|
||||
while (delta > 180f) delta -= 360f;
|
||||
while (delta < -180f) delta += 360f;
|
||||
return Math.Abs(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
|
|
@ -75,5 +76,28 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
double dy = target.Y - pos.Y;
|
||||
return dx * dx + dz * dz < horizThresholdSq && Math.Abs(dy) < vertThreshold;
|
||||
}
|
||||
|
||||
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment)
|
||||
{
|
||||
float headingYaw = CalculateYaw(segment.HeadingX, segment.HeadingZ);
|
||||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void ApplyDecision(MovementInput input, TransitionBrakingDecision decision)
|
||||
{
|
||||
input.Forward = decision.HoldForward;
|
||||
input.Sprint = decision.HoldSprint;
|
||||
input.Back = decision.HoldBack;
|
||||
}
|
||||
|
||||
internal static bool IsSettledAtEnd(Location pos, Location target, PlayerPhysics physics,
|
||||
double horizThresholdSq = 0.0025, double speedThresholdSq = 0.0016)
|
||||
{
|
||||
double dx = target.X - pos.X;
|
||||
double dz = target.Z - pos.Z;
|
||||
double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X
|
||||
+ physics.DeltaMovement.Z * physics.DeltaMovement.Z;
|
||||
return dx * dx + dz * dz <= horizThresholdSq && horizontalSpeedSq <= speedThresholdSq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,22 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private readonly PathSegment _segment;
|
||||
private readonly PathSegment? _nextSegment;
|
||||
private int _tickCount;
|
||||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
|
||||
public WalkTemplate(Location start, Location end)
|
||||
public WalkTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
_lastPos = start;
|
||||
_segment = segment;
|
||||
_nextSegment = nextSegment;
|
||||
ExpectedStart = segment.Start;
|
||||
ExpectedEnd = segment.End;
|
||||
_lastPos = segment.Start;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
|
|
@ -35,17 +39,24 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
if (TemplateHelper.IsNear(pos, ExpectedEnd, horizThresholdSq: 0.20))
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
TemplateHelper.FaceSegmentHeading(physics, _segment);
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.ContinueStraight && TemplateHelper.IsNear(pos, ExpectedEnd, horizThresholdSq: 0.09))
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_segment.ExitTransition != PathTransitionType.ContinueStraight && TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics))
|
||||
return TemplateState.Complete;
|
||||
|
||||
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);
|
||||
_stuckTicks = movedSq < 0.0005 ? _stuckTicks + 1 : 0;
|
||||
_lastPos = pos;
|
||||
|
||||
if (_stuckTicks > 40 || _tickCount > 100)
|
||||
int maxTicks = _segment.ExitTransition == PathTransitionType.ContinueStraight ? 100 : 140;
|
||||
if (_stuckTicks > 40 || _tickCount > maxTicks)
|
||||
return TemplateState.Failed;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public readonly record struct TransitionBrakingDecision(bool HoldForward, bool HoldSprint, bool HoldBack)
|
||||
{
|
||||
public static TransitionBrakingDecision CarryMomentum(bool preserveSprint) =>
|
||||
new(true, preserveSprint, false);
|
||||
|
||||
public static TransitionBrakingDecision Coast =>
|
||||
new(false, false, false);
|
||||
|
||||
public static TransitionBrakingDecision Brake =>
|
||||
new(false, false, true);
|
||||
}
|
||||
}
|
||||
107
MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs
Normal file
107
MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public static class TransitionBrakingPlanner
|
||||
{
|
||||
private const double GroundSpeedThreshold = 0.025;
|
||||
private const int MaxSimulationTicks = 14;
|
||||
private const double FinalStopLead = 0.06;
|
||||
private const double FinalBrakeLead = 0.04;
|
||||
private const double TurnBrakeLead = 0.10;
|
||||
private const double AirReleaseLead = 0.14;
|
||||
|
||||
public static TransitionBrakingDecision Plan(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
if (current.ExitTransition is PathTransitionType.ContinueStraight or PathTransitionType.PrepareJump)
|
||||
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
|
||||
|
||||
double remaining = RemainingDistanceAlongSegment(current, pos);
|
||||
double forwardSpeed = Math.Max(0.0, ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ));
|
||||
double coastStopDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: false);
|
||||
double hardBrakeDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: true);
|
||||
|
||||
if (current.ExitTransition == PathTransitionType.FinalStop)
|
||||
{
|
||||
if (remaining < 0.0)
|
||||
return TransitionBrakingDecision.Brake;
|
||||
|
||||
if (forwardSpeed > GroundSpeedThreshold && remaining <= hardBrakeDistance + FinalBrakeLead)
|
||||
return TransitionBrakingDecision.Brake;
|
||||
|
||||
if (forwardSpeed <= GroundSpeedThreshold && remaining > 0.0)
|
||||
return TransitionBrakingDecision.CarryMomentum(preserveSprint: false);
|
||||
}
|
||||
|
||||
if (current.ExitTransition == PathTransitionType.Turn && remaining <= hardBrakeDistance + TurnBrakeLead)
|
||||
{
|
||||
return TransitionBrakingDecision.Brake;
|
||||
}
|
||||
|
||||
if (remaining <= coastStopDistance + FinalStopLead)
|
||||
return TransitionBrakingDecision.Coast;
|
||||
|
||||
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
|
||||
}
|
||||
|
||||
public static bool ShouldReleaseForwardInAir(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics)
|
||||
{
|
||||
if (current.ExitTransition is not (PathTransitionType.FinalStop or PathTransitionType.Turn or PathTransitionType.LandingRecovery))
|
||||
return false;
|
||||
|
||||
double remaining = RemainingDistanceAlongSegment(current, pos);
|
||||
double forwardSpeed = Math.Max(0.0, ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ));
|
||||
|
||||
return remaining <= forwardSpeed + AirReleaseLead;
|
||||
}
|
||||
|
||||
public static double EstimateGroundStopDistance(PlayerPhysics physics, World world, int headingX, int headingZ, bool applyBackBrake)
|
||||
{
|
||||
if (!physics.OnGround)
|
||||
return 0.0;
|
||||
|
||||
double forwardSpeed = Math.Max(0.0, ProjectHorizontalSpeedAlongHeading(physics, headingX, headingZ));
|
||||
if (forwardSpeed <= GroundSpeedThreshold)
|
||||
return 0.0;
|
||||
|
||||
float blockFriction = PlayerPhysics.GetMaterialFriction(
|
||||
world.GetBlock(new Location(physics.Position.X, physics.Position.Y - 0.5000010, physics.Position.Z)).Type);
|
||||
double drag = blockFriction * PhysicsConsts.FrictionMultiplier;
|
||||
double acceleration = physics.MovementSpeed
|
||||
* (PhysicsConsts.GroundAccelerationFactor / (drag * drag * drag))
|
||||
* PhysicsConsts.InputFriction;
|
||||
|
||||
if (applyBackBrake)
|
||||
acceleration *= 0.98;
|
||||
|
||||
double distance = 0.0;
|
||||
double speed = forwardSpeed;
|
||||
for (int tick = 0; tick < MaxSimulationTicks; tick++)
|
||||
{
|
||||
distance += speed;
|
||||
speed = applyBackBrake
|
||||
? Math.Max(0.0, (speed - acceleration) * drag)
|
||||
: speed * drag;
|
||||
|
||||
if (speed <= GroundSpeedThreshold)
|
||||
break;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
private static double RemainingDistanceAlongSegment(PathSegment current, Location pos)
|
||||
{
|
||||
double dx = current.End.X - pos.X;
|
||||
double dz = current.End.Z - pos.Z;
|
||||
return dx * current.HeadingX + dz * current.HeadingZ;
|
||||
}
|
||||
|
||||
private static double ProjectHorizontalSpeedAlongHeading(PlayerPhysics physics, int headingX, int headingZ)
|
||||
{
|
||||
return physics.DeltaMovement.X * headingX + physics.DeltaMovement.Z * headingZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue