mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Add pathing contract loader validation scaffold
This commit is contained in:
parent
aefc7235ee
commit
9fe376a3bb
6 changed files with 376 additions and 52 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
|
||||
|
|
@ -21,21 +22,36 @@ public sealed class PathingContractStore
|
|||
string rootPath = FindRepositoryRoot();
|
||||
string pathingDir = Path.Combine(rootPath, "MinecraftClient.Tests", "TestData", "Pathing");
|
||||
|
||||
string plannerJson = File.ReadAllText(Path.Combine(pathingDir, "pathing-planner-contracts.json"));
|
||||
string timingJson = File.ReadAllText(Path.Combine(pathingDir, "pathing-timing-budgets.json"));
|
||||
|
||||
return LoadFromJson(plannerJson, timingJson);
|
||||
}
|
||||
|
||||
public static PathingContractStore LoadFromJson(string plannerJson, string timingJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(plannerJson);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(timingJson);
|
||||
|
||||
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)
|
||||
PathingPlannerContract[] plannerContracts = JsonSerializer.Deserialize<PathingPlannerContract[]>(plannerJson, options)
|
||||
?? throw new InvalidOperationException("Failed to deserialize planner contracts.");
|
||||
Dictionary<string, PathingTimingBudget> timingBudgets = JsonSerializer.Deserialize<Dictionary<string, PathingTimingBudget>>(timingJson, options)
|
||||
PathingTimingBudget[] timingBudgets = JsonSerializer.Deserialize<PathingTimingBudget[]>(timingJson, options)
|
||||
?? throw new InvalidOperationException("Failed to deserialize timing budgets.");
|
||||
|
||||
return new PathingContractStore(plannerContracts, timingBudgets);
|
||||
Dictionary<string, PathingPlannerContract> plannerByScenario = BuildPlannerDictionary(plannerContracts);
|
||||
Dictionary<string, PathingTimingBudget> timingByScenario = BuildTimingDictionary(timingBudgets);
|
||||
ValidateScenarioSetConsistency(plannerByScenario, timingByScenario);
|
||||
ValidateScenarioSegmentAlignment(plannerByScenario, timingByScenario);
|
||||
|
||||
return new PathingContractStore(
|
||||
plannerByScenario,
|
||||
timingByScenario);
|
||||
}
|
||||
|
||||
public PathingPlannerContract GetPlanner(string id)
|
||||
|
|
@ -68,4 +84,120 @@ public sealed class PathingContractStore
|
|||
|
||||
throw new DirectoryNotFoundException("Unable to locate repository root from current test execution directory.");
|
||||
}
|
||||
|
||||
private static Dictionary<string, PathingPlannerContract> BuildPlannerDictionary(IEnumerable<PathingPlannerContract> contracts)
|
||||
{
|
||||
var result = new Dictionary<string, PathingPlannerContract>(StringComparer.Ordinal);
|
||||
foreach (PathingPlannerContract contract in contracts)
|
||||
{
|
||||
PathingPlannerContract normalized = ValidateAndNormalizePlanner(contract);
|
||||
if (!result.TryAdd(normalized.ScenarioId, normalized))
|
||||
throw new InvalidDataException($"Duplicate planner contract scenario id '{normalized.ScenarioId}'.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, PathingTimingBudget> BuildTimingDictionary(IEnumerable<PathingTimingBudget> budgets)
|
||||
{
|
||||
var result = new Dictionary<string, PathingTimingBudget>(StringComparer.Ordinal);
|
||||
foreach (PathingTimingBudget budget in budgets)
|
||||
{
|
||||
PathingTimingBudget normalized = ValidateAndNormalizeTiming(budget);
|
||||
if (!result.TryAdd(normalized.ScenarioId, normalized))
|
||||
throw new InvalidDataException($"Duplicate timing budget scenario id '{normalized.ScenarioId}'.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PathingPlannerContract ValidateAndNormalizePlanner(PathingPlannerContract contract)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contract.ScenarioId))
|
||||
throw new InvalidDataException("Planner contract has blank scenario id.");
|
||||
|
||||
if (contract.Segments is null || contract.Segments.Count == 0)
|
||||
throw new InvalidDataException($"Planner contract '{contract.ScenarioId}' must contain at least one segment.");
|
||||
|
||||
var normalizedSegments = new List<PathingPlannerSegmentContract>(contract.Segments.Count);
|
||||
for (int i = 0; i < contract.Segments.Count; i++)
|
||||
{
|
||||
PathingPlannerSegmentContract segment = contract.Segments[i];
|
||||
normalizedSegments.Add(segment);
|
||||
}
|
||||
|
||||
return contract with { Segments = normalizedSegments.AsReadOnly() };
|
||||
}
|
||||
|
||||
private static PathingTimingBudget ValidateAndNormalizeTiming(PathingTimingBudget budget)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(budget.ScenarioId))
|
||||
throw new InvalidDataException("Timing budget has blank scenario id.");
|
||||
|
||||
if (budget.Segments is null || budget.Segments.Count == 0)
|
||||
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' must contain at least one segment.");
|
||||
|
||||
if (budget.ExpectedTotalTicks < 0 || budget.MaxTotalTicks < 0)
|
||||
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' total ticks must be nonnegative.");
|
||||
|
||||
if (budget.ExpectedTotalTicks > budget.MaxTotalTicks)
|
||||
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' has ExpectedTotalTicks greater than MaxTotalTicks.");
|
||||
|
||||
var normalizedSegments = new List<PathingSegmentTimingBudget>(budget.Segments.Count);
|
||||
for (int i = 0; i < budget.Segments.Count; i++)
|
||||
{
|
||||
PathingSegmentTimingBudget segment = budget.Segments[i];
|
||||
if (segment.ExpectedTicks < 0 || segment.MaxTicks < 0)
|
||||
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' segment {i} has negative tick values.");
|
||||
|
||||
if (segment.ExpectedTicks > segment.MaxTicks)
|
||||
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' segment {i} has ExpectedTicks greater than MaxTicks.");
|
||||
|
||||
normalizedSegments.Add(segment);
|
||||
}
|
||||
|
||||
return budget with { Segments = normalizedSegments.AsReadOnly() };
|
||||
}
|
||||
|
||||
private static void ValidateScenarioSetConsistency(
|
||||
IReadOnlyDictionary<string, PathingPlannerContract> plannersByScenario,
|
||||
IReadOnlyDictionary<string, PathingTimingBudget> timingsByScenario)
|
||||
{
|
||||
string[] plannerOnly = plannersByScenario.Keys.Except(timingsByScenario.Keys, StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal).ToArray();
|
||||
string[] timingOnly = timingsByScenario.Keys.Except(plannersByScenario.Keys, StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal).ToArray();
|
||||
|
||||
if (plannerOnly.Length == 0 && timingOnly.Length == 0)
|
||||
return;
|
||||
|
||||
string plannerOnlyList = plannerOnly.Length == 0 ? "<none>" : string.Join(", ", plannerOnly);
|
||||
string timingOnlyList = timingOnly.Length == 0 ? "<none>" : string.Join(", ", timingOnly);
|
||||
throw new InvalidDataException(
|
||||
$"Planner/timing scenario set mismatch. Missing timing entries for: {plannerOnlyList}. Missing planner entries for: {timingOnlyList}.");
|
||||
}
|
||||
|
||||
private static void ValidateScenarioSegmentAlignment(
|
||||
IReadOnlyDictionary<string, PathingPlannerContract> plannersByScenario,
|
||||
IReadOnlyDictionary<string, PathingTimingBudget> timingsByScenario)
|
||||
{
|
||||
foreach ((string scenarioId, PathingPlannerContract planner) in plannersByScenario)
|
||||
{
|
||||
PathingTimingBudget timing = timingsByScenario[scenarioId];
|
||||
if (planner.Segments.Count != timing.Segments.Count)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Scenario '{scenarioId}' segment count mismatch. Planner has {planner.Segments.Count} segments, timing has {timing.Segments.Count}.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < planner.Segments.Count; i++)
|
||||
{
|
||||
MoveType plannerMove = planner.Segments[i].MoveType;
|
||||
MoveType timingMove = timing.Segments[i].MoveType;
|
||||
if (plannerMove != timingMove)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Scenario '{scenarioId}' segment {i} move mismatch. Planner has {plannerMove}, timing has {timingMove}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ 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);
|
||||
MoveType MoveType,
|
||||
PathingBlock StartBlock,
|
||||
PathingBlock EndBlock);
|
||||
|
||||
public sealed record PathingPlannerContract(
|
||||
string ScenarioId,
|
||||
PathStatus ExpectedStatus,
|
||||
PathingPlannerSegmentContract[] Segments);
|
||||
IReadOnlyList<PathingPlannerSegmentContract> Segments);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ using MinecraftClient.Pathing.Core;
|
|||
namespace MinecraftClient.Tests.Pathing.Execution.Contracts;
|
||||
|
||||
public sealed record PathingSegmentTimingBudget(
|
||||
MoveType Move,
|
||||
int BudgetMs);
|
||||
MoveType MoveType,
|
||||
int ExpectedTicks,
|
||||
int MaxTicks);
|
||||
|
||||
public sealed record PathingTimingBudget(
|
||||
int TotalBudgetMs,
|
||||
PathingSegmentTimingBudget[] Segments);
|
||||
string ScenarioId,
|
||||
int ExpectedTotalTicks,
|
||||
int MaxTotalTicks,
|
||||
IReadOnlyList<PathingSegmentTimingBudget> Segments);
|
||||
|
|
|
|||
|
|
@ -13,17 +13,202 @@ public sealed class PathPlanningContractTests
|
|||
|
||||
PathingPlannerContract contract = store.GetPlanner("manager-accepted-ascend-chain");
|
||||
|
||||
Assert.Equal("manager-accepted-ascend-chain", contract.ScenarioId);
|
||||
Assert.Equal(PathStatus.Success, contract.ExpectedStatus);
|
||||
Assert.Equal(6, contract.Segments.Length);
|
||||
Assert.Equal(6, contract.Segments.Count);
|
||||
|
||||
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);
|
||||
Assert.Equal(MoveType.Diagonal, firstSegment.MoveType);
|
||||
Assert.Equal(new PathingBlock(171, 80, 160), firstSegment.StartBlock);
|
||||
Assert.Equal(new PathingBlock(172, 80, 161), firstSegment.EndBlock);
|
||||
|
||||
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);
|
||||
Assert.Equal(MoveType.Ascend, lastSegment.MoveType);
|
||||
Assert.Equal(new PathingBlock(176, 82, 162), lastSegment.StartBlock);
|
||||
Assert.Equal(new PathingBlock(177, 83, 162), lastSegment.EndBlock);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_ManagerAcceptedAscendChain_LoadsTimingScaffoldShape()
|
||||
{
|
||||
var store = PathingContractStore.LoadFromRepositoryRoot();
|
||||
|
||||
PathingTimingBudget timing = store.GetTiming("manager-accepted-ascend-chain");
|
||||
|
||||
Assert.Equal("manager-accepted-ascend-chain", timing.ScenarioId);
|
||||
Assert.Equal(6, timing.Segments.Count);
|
||||
Assert.True(timing.ExpectedTotalTicks <= timing.MaxTotalTicks);
|
||||
|
||||
PathingSegmentTimingBudget firstSegment = timing.Segments[0];
|
||||
Assert.Equal(MoveType.Diagonal, firstSegment.MoveType);
|
||||
Assert.True(firstSegment.ExpectedTicks <= firstSegment.MaxTicks);
|
||||
|
||||
foreach (PathingSegmentTimingBudget segment in timing.Segments)
|
||||
{
|
||||
Assert.True(segment.ExpectedTicks <= segment.MaxTicks);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromJson_RejectsTimingSegment_WhenExpectedExceedsMax()
|
||||
{
|
||||
const string plannerJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "manager-accepted-ascend-chain",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Diagonal",
|
||||
"startBlock": { "x": 171, "y": 80, "z": 160 },
|
||||
"endBlock": { "x": 172, "y": 80, "z": 161 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
const string timingJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "manager-accepted-ascend-chain",
|
||||
"expectedTotalTicks": 1,
|
||||
"maxTotalTicks": 1,
|
||||
"segments": [
|
||||
{ "moveType": "Diagonal", "expectedTicks": 2, "maxTicks": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
InvalidDataException error = Assert.Throws<InvalidDataException>(
|
||||
() => PathingContractStore.LoadFromJson(plannerJson, timingJson));
|
||||
Assert.Contains("manager-accepted-ascend-chain", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromJson_Rejects_WhenPlannerAndTimingScenarioSetsMismatch()
|
||||
{
|
||||
const string plannerJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "planner-only",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Traverse",
|
||||
"startBlock": { "x": 0, "y": 80, "z": 0 },
|
||||
"endBlock": { "x": 1, "y": 80, "z": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
const string timingJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "timing-only",
|
||||
"expectedTotalTicks": 1,
|
||||
"maxTotalTicks": 1,
|
||||
"segments": [
|
||||
{ "moveType": "Traverse", "expectedTicks": 1, "maxTicks": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
InvalidDataException error = Assert.Throws<InvalidDataException>(
|
||||
() => PathingContractStore.LoadFromJson(plannerJson, timingJson));
|
||||
Assert.Contains("planner-only", error.Message);
|
||||
Assert.Contains("timing-only", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromJson_RejectsDuplicatePlannerScenarioId()
|
||||
{
|
||||
const string plannerJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "dup",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Traverse",
|
||||
"startBlock": { "x": 0, "y": 80, "z": 0 },
|
||||
"endBlock": { "x": 1, "y": 80, "z": 0 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenarioId": "dup",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Traverse",
|
||||
"startBlock": { "x": 1, "y": 80, "z": 0 },
|
||||
"endBlock": { "x": 2, "y": 80, "z": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
const string timingJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "dup",
|
||||
"expectedTotalTicks": 1,
|
||||
"maxTotalTicks": 1,
|
||||
"segments": [
|
||||
{ "moveType": "Traverse", "expectedTicks": 1, "maxTicks": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
InvalidDataException error = Assert.Throws<InvalidDataException>(
|
||||
() => PathingContractStore.LoadFromJson(plannerJson, timingJson));
|
||||
Assert.Contains("Duplicate planner contract scenario id", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromJson_Rejects_WhenPlannerAndTimingMoveSequenceDiffer()
|
||||
{
|
||||
const string plannerJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "sequence-mismatch",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Traverse",
|
||||
"startBlock": { "x": 0, "y": 80, "z": 0 },
|
||||
"endBlock": { "x": 1, "y": 80, "z": 0 }
|
||||
},
|
||||
{
|
||||
"moveType": "Ascend",
|
||||
"startBlock": { "x": 1, "y": 80, "z": 0 },
|
||||
"endBlock": { "x": 2, "y": 81, "z": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
const string timingJson = """
|
||||
[
|
||||
{
|
||||
"scenarioId": "sequence-mismatch",
|
||||
"expectedTotalTicks": 2,
|
||||
"maxTotalTicks": 2,
|
||||
"segments": [
|
||||
{ "moveType": "Traverse", "expectedTicks": 1, "maxTicks": 1 },
|
||||
{ "moveType": "Diagonal", "expectedTicks": 1, "maxTicks": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
InvalidDataException error = Assert.Throws<InvalidDataException>(
|
||||
() => PathingContractStore.LoadFromJson(plannerJson, timingJson));
|
||||
Assert.Contains("sequence-mismatch", error.Message);
|
||||
Assert.Contains("segment 1", error.Message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,38 @@
|
|||
{
|
||||
"manager-accepted-ascend-chain": {
|
||||
[
|
||||
{
|
||||
"scenarioId": "manager-accepted-ascend-chain",
|
||||
"expectedStatus": "Success",
|
||||
"segments": [
|
||||
{
|
||||
"move": "Diagonal",
|
||||
"from": { "x": 171, "y": 80, "z": 160 },
|
||||
"to": { "x": 172, "y": 80, "z": 161 }
|
||||
"moveType": "Diagonal",
|
||||
"startBlock": { "x": 171, "y": 80, "z": 160 },
|
||||
"endBlock": { "x": 172, "y": 80, "z": 161 }
|
||||
},
|
||||
{
|
||||
"move": "Diagonal",
|
||||
"from": { "x": 172, "y": 80, "z": 161 },
|
||||
"to": { "x": 173, "y": 80, "z": 162 }
|
||||
"moveType": "Diagonal",
|
||||
"startBlock": { "x": 172, "y": 80, "z": 161 },
|
||||
"endBlock": { "x": 173, "y": 80, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Traverse",
|
||||
"from": { "x": 173, "y": 80, "z": 162 },
|
||||
"to": { "x": 174, "y": 80, "z": 162 }
|
||||
"moveType": "Traverse",
|
||||
"startBlock": { "x": 173, "y": 80, "z": 162 },
|
||||
"endBlock": { "x": 174, "y": 80, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 174, "y": 80, "z": 162 },
|
||||
"to": { "x": 175, "y": 81, "z": 162 }
|
||||
"moveType": "Ascend",
|
||||
"startBlock": { "x": 174, "y": 80, "z": 162 },
|
||||
"endBlock": { "x": 175, "y": 81, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 175, "y": 81, "z": 162 },
|
||||
"to": { "x": 176, "y": 82, "z": 162 }
|
||||
"moveType": "Ascend",
|
||||
"startBlock": { "x": 175, "y": 81, "z": 162 },
|
||||
"endBlock": { "x": 176, "y": 82, "z": 162 }
|
||||
},
|
||||
{
|
||||
"move": "Ascend",
|
||||
"from": { "x": 176, "y": 82, "z": 162 },
|
||||
"to": { "x": 177, "y": 83, "z": 162 }
|
||||
"moveType": "Ascend",
|
||||
"startBlock": { "x": 176, "y": 82, "z": 162 },
|
||||
"endBlock": { "x": 177, "y": 83, "z": 162 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"manager-accepted-ascend-chain": {
|
||||
"totalBudgetMs": 0,
|
||||
[
|
||||
{
|
||||
"scenarioId": "manager-accepted-ascend-chain",
|
||||
"expectedTotalTicks": 0,
|
||||
"maxTotalTicks": 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 }
|
||||
{ "moveType": "Diagonal", "expectedTicks": 0, "maxTicks": 0 },
|
||||
{ "moveType": "Diagonal", "expectedTicks": 0, "maxTicks": 0 },
|
||||
{ "moveType": "Traverse", "expectedTicks": 0, "maxTicks": 0 },
|
||||
{ "moveType": "Ascend", "expectedTicks": 0, "maxTicks": 0 },
|
||||
{ "moveType": "Ascend", "expectedTicks": 0, "maxTicks": 0 },
|
||||
{ "moveType": "Ascend", "expectedTicks": 0, "maxTicks": 0 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue