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
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue