< Summary

Information
Class: Trailblazer.Pathing.NavigationMapTokenImporter
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Map/NavigationMapTokenImporter.cs
Line coverage
100%
Covered lines: 202
Uncovered lines: 0
Coverable lines: 202
Total lines: 360
Line coverage: 100%
Branch coverage
100%
Covered branches: 110
Total branches: 110
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ImportRectangular(...)100%1616100%
ParseCell(...)100%3232100%
GenerateTransitions(...)100%1212100%
GenerateTransitionPair(...)100%44100%
TryAddMediaTransitionPair(...)100%2020100%
TrySelectBoundary(...)100%88100%
TryAddClimbTransitionPair(...)100%1818100%
CreateTransition(...)100%11100%
CreateTransitionId(...)100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Map/NavigationMapTokenImporter.cs

#LineLine coverage
 1//=======================================================================
 2// NavigationMapTokenImporter.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using System.Globalization;
 10using FixedMathSharp;
 11using GridForge.Configuration;
 12using GridForge.Grids.Topology;
 13using GridForge.Spatial;
 14
 15namespace Trailblazer.Pathing;
 16
 17/// <summary>
 18/// Imports X/Y/Z rectangular token maps into immutable addressed navigation maps.
 19/// </summary>
 20public static class NavigationMapTokenImporter
 21{
 22    /// <summary>
 23    /// Parses a rectangular token volume and emits explicit canonical cells and addressed
 24    /// semantic transitions. Token coverage is not retained as a default-cell mask.
 25    /// </summary>
 26    public static NavigationMap ImportRectangular(
 27        string mapId,
 28        GridConfiguration gridConfiguration,
 29        string[,,] source,
 30        NavigationTokenLegend? legend = null,
 31        string? transitionIdPrefix = null)
 32    {
 3033        SwiftThrowHelper.ThrowIfNull(source, nameof(source));
 2934        var builder = new NavigationMapBuilder(mapId, gridConfiguration);
 2935        SwiftThrowHelper.ThrowIfArgument(
 2936            builder.GridBinding.Configuration.TopologyKind != GridTopologyKind.RectangularPrism,
 2937            nameof(gridConfiguration),
 2938            "Token import requires rectangular-prism topology.");
 2939        SwiftThrowHelper.ThrowIfArgument(
 2940            source.GetLength(0) != builder.GridBinding.Width
 2941            || source.GetLength(1) != builder.GridBinding.Height
 2942            || source.GetLength(2) != builder.GridBinding.Length,
 2943            nameof(source),
 2944            "Token dimensions must match the normalized grid address space in X/Y/Z order.");
 45
 2646        NavigationTokenLegend resolvedLegend = legend ?? NavigationTokenLegend.CreateBuiltIn();
 2647        string resolvedPrefix = string.IsNullOrWhiteSpace(transitionIdPrefix)
 2648            ? mapId
 2649            : transitionIdPrefix!;
 2650        var parsed = new ParsedTokenCell[
 2651            builder.GridBinding.Width,
 2652            builder.GridBinding.Height,
 2653            builder.GridBinding.Length];
 54
 12455        for (int x = 0; x < parsed.GetLength(0); x++)
 56        {
 16257            for (int y = 0; y < parsed.GetLength(1); y++)
 58            {
 16859                for (int z = 0; z < parsed.GetLength(2); z++)
 60                {
 4761                    ParsedTokenCell cell = ParseCell(source[x, y, z], x, y, z, resolvedLegend);
 3962                    parsed[x, y, z] = cell;
 3963                    if (cell.Entry.EmitsCell)
 3564                        builder.AddCell(new VoxelIndex(x, y, z), cell.Cell);
 65                }
 66            }
 67        }
 68
 1869        GenerateTransitions(builder, parsed, mapId, resolvedPrefix);
 1870        return builder.Build();
 71    }
 72
 73    private static ParsedTokenCell ParseCell(
 74        string rawToken,
 75        int x,
 76        int y,
 77        int z,
 78        NavigationTokenLegend legend)
 79    {
 4780        string token = rawToken?.Trim() ?? string.Empty;
 4781        bool hasMarker = false;
 4782        int markerIndex = token.IndexOf('!');
 4783        if (markerIndex >= 0)
 84        {
 2885            SwiftThrowHelper.ThrowIfArgument(
 2886                markerIndex != token.Length - 1 || token.LastIndexOf('!') != markerIndex,
 2887                nameof(rawToken),
 2888                $"Invalid token '{token}' at [{x}, {y}, {z}]. Only one trailing '!' marker is supported.");
 2789            hasMarker = true;
 2790            token = token[..^1].TrimEnd();
 2791            SwiftThrowHelper.ThrowIfArgument(
 2792                token.Length == 0,
 2793                nameof(rawToken),
 2794                $"Invalid token '{rawToken}' at [{x}, {y}, {z}]. A marker requires a base token.");
 95        }
 96
 4597        bool hasInlineCost = false;
 4598        Fixed64 inlineCost = default;
 4599        int costSeparatorIndex = token.LastIndexOf('_');
 45100        if (costSeparatorIndex >= 0)
 101        {
 5102            string costText = token[(costSeparatorIndex + 1)..];
 5103            bool parsed = Fixed64.TryParse(costText, CultureInfo.InvariantCulture, out inlineCost);
 5104            SwiftThrowHelper.ThrowIfArgument(
 5105                !parsed || inlineCost < Fixed64.Zero || costSeparatorIndex == 0,
 5106                nameof(rawToken),
 5107                $"Invalid non-negative fixed-point cost in token '{rawToken}' at [{x}, {y}, {z}].");
 2108            hasInlineCost = true;
 2109            token = token[..costSeparatorIndex].TrimEnd();
 110        }
 111
 42112        bool known = legend.TryGetEntry(token, out NavigationTokenLegendEntry entry);
 42113        SwiftThrowHelper.ThrowIfArgument(
 42114            !known,
 42115            nameof(rawToken),
 42116            $"Unknown navigation token '{rawToken}' at [{x}, {y}, {z}].");
 41117        SwiftThrowHelper.ThrowIfArgument(
 41118            hasMarker
 41119            && (!entry.EmitsCell
 41120                || (entry.TransitionMedia == TraversalMedia.None
 41121                    && (entry.Cell.Flags & NavigationCellFlags.ClimbSurfaceHint) == 0)),
 41122            nameof(rawToken),
 41123            $"Token '{rawToken}' at [{x}, {y}, {z}] cannot generate transitions.");
 124
 39125        if (!entry.EmitsCell)
 4126            return default;
 127
 35128        return new ParsedTokenCell(
 35129            entry,
 35130            entry.CreateCell(inlineCost, hasInlineCost, hasMarker),
 35131            hasMarker);
 132    }
 133
 134    private static void GenerateTransitions(
 135        NavigationMapBuilder builder,
 136        ParsedTokenCell[,,] cells,
 137        string mapId,
 138        string transitionIdPrefix)
 139    {
 108140        for (int x = 0; x < cells.GetLength(0); x++)
 141        {
 146142            for (int y = 0; y < cells.GetLength(1); y++)
 143            {
 152144                for (int z = 0; z < cells.GetLength(2); z++)
 145                {
 39146                    VoxelIndex source = new(x, y, z);
 39147                    if (x + 1 < cells.GetLength(0))
 18148                        GenerateTransitionPair(builder, cells[x, y, z], cells[x + 1, y, z], source, new VoxelIndex(x + 1
 39149                    if (y + 1 < cells.GetLength(1))
 2150                        GenerateTransitionPair(builder, cells[x, y, z], cells[x, y + 1, z], source, new VoxelIndex(x, y 
 39151                    if (z + 1 < cells.GetLength(2))
 2152                        GenerateTransitionPair(builder, cells[x, y, z], cells[x, y, z + 1], source, new VoxelIndex(x, y,
 153                }
 154            }
 155        }
 18156    }
 157
 158    private static void GenerateTransitionPair(
 159        NavigationMapBuilder builder,
 160        ParsedTokenCell first,
 161        ParsedTokenCell second,
 162        VoxelIndex firstIndex,
 163        VoxelIndex secondIndex,
 164        string mapId,
 165        string prefix)
 166    {
 22167        if (!first.Entry.EmitsCell || !second.Entry.EmitsCell)
 4168            return;
 169
 18170        TryAddMediaTransitionPair(builder, first, second, firstIndex, secondIndex, mapId, prefix);
 18171        TryAddClimbTransitionPair(builder, first, second, firstIndex, secondIndex, mapId, prefix);
 18172    }
 173
 174    private static void TryAddMediaTransitionPair(
 175        NavigationMapBuilder builder,
 176        ParsedTokenCell first,
 177        ParsedTokenCell second,
 178        VoxelIndex firstIndex,
 179        VoxelIndex secondIndex,
 180        string mapId,
 181        string prefix)
 182    {
 18183        TraversalMedia firstMarkerMedia = first.HasMarker ? first.Entry.TransitionMedia : TraversalMedia.None;
 18184        TraversalMedia secondMarkerMedia = second.HasMarker ? second.Entry.TransitionMedia : TraversalMedia.None;
 18185        int candidateCount = 0;
 18186        VoxelIndex solidIndex = default;
 18187        VoxelIndex volumeIndex = default;
 18188        TraversalMedium volumeMedium = default;
 189
 18190        TrySelectBoundary(firstMarkerMedia, secondMarkerMedia, TraversalMedia.Gas, TraversalMedium.Gas, firstIndex, seco
 18191        TrySelectBoundary(firstMarkerMedia, secondMarkerMedia, TraversalMedia.Liquid, TraversalMedium.Liquid, firstIndex
 18192        if (candidateCount != 1)
 7193            return;
 194
 11195        TraversalTransitionType entryType = volumeMedium == TraversalMedium.Gas
 11196            ? TraversalTransitionType.Takeoff
 11197            : TraversalTransitionType.SwimEntry;
 11198        TraversalTransitionType exitType = volumeMedium == TraversalMedium.Gas
 11199            ? TraversalTransitionType.Landing
 11200            : TraversalTransitionType.SwimExit;
 11201        TraversalCapability capability = volumeMedium == TraversalMedium.Gas
 11202            ? TraversalCapability.Fly
 11203            : TraversalCapability.Swim;
 11204        ParsedTokenCell solid = solidIndex == firstIndex ? first : second;
 11205        TraversalTransitionLocomotionHints exitHints =
 11206            volumeMedium == TraversalMedium.Liquid
 11207            && (solid.Cell.Media & TraversalMedia.Liquid) != 0
 11208            && (solid.Cell.Flags & NavigationCellFlags.ClimbSurfaceHint) != 0
 11209                ? TraversalTransitionLocomotionHints.RequestClimb
 11210                    | TraversalTransitionLocomotionHints.PreserveClimbAfterCompletion
 11211                : TraversalTransitionLocomotionHints.None;
 11212        builder.AddTransition(CreateTransition(prefix, entryType, solidIndex, TraversalMedium.Solid, volumeIndex, volume
 11213        builder.AddTransition(CreateTransition(
 11214            prefix,
 11215            exitType,
 11216            volumeIndex,
 11217            volumeMedium,
 11218            solidIndex,
 11219            TraversalMedium.Solid,
 11220            mapId,
 11221            capability,
 11222            locomotionHints: exitHints));
 11223    }
 224
 225    private static void TrySelectBoundary(
 226        TraversalMedia firstMedia,
 227        TraversalMedia secondMedia,
 228        TraversalMedia volumeFlag,
 229        TraversalMedium volumeMediumCandidate,
 230        VoxelIndex firstIndex,
 231        VoxelIndex secondIndex,
 232        ref int candidateCount,
 233        ref VoxelIndex solidIndex,
 234        ref VoxelIndex volumeIndex,
 235        ref TraversalMedium volumeMedium)
 236    {
 36237        if ((firstMedia & TraversalMedia.Solid) != 0 && (secondMedia & volumeFlag) != 0)
 238        {
 4239            candidateCount++;
 4240            solidIndex = firstIndex;
 4241            volumeIndex = secondIndex;
 4242            volumeMedium = volumeMediumCandidate;
 243        }
 244
 36245        if ((secondMedia & TraversalMedia.Solid) != 0 && (firstMedia & volumeFlag) != 0)
 246        {
 7247            candidateCount++;
 7248            solidIndex = secondIndex;
 7249            volumeIndex = firstIndex;
 7250            volumeMedium = volumeMediumCandidate;
 251        }
 36252    }
 253
 254    private static void TryAddClimbTransitionPair(
 255        NavigationMapBuilder builder,
 256        ParsedTokenCell first,
 257        ParsedTokenCell second,
 258        VoxelIndex firstIndex,
 259        VoxelIndex secondIndex,
 260        string mapId,
 261        string prefix)
 262    {
 18263        bool firstClimb = (first.Cell.Flags & NavigationCellFlags.ClimbSurfaceHint) != 0;
 18264        bool secondClimb = (second.Cell.Flags & NavigationCellFlags.ClimbSurfaceHint) != 0;
 18265        bool connect = (firstClimb && secondClimb)
 18266            || (firstClimb && first.HasMarker && (second.Cell.Media & TraversalMedia.Solid) != 0)
 18267            || (secondClimb && second.HasMarker && (first.Cell.Media & TraversalMedia.Solid) != 0);
 18268        if (!connect)
 15269            return;
 270
 3271        TraversalTransitionLocomotionHints firstToSecondHints =
 3272            TraversalTransitionLocomotionHints.RequestClimb
 3273            | (secondClimb
 3274                ? TraversalTransitionLocomotionHints.PreserveClimbAfterCompletion
 3275                : TraversalTransitionLocomotionHints.None);
 3276        TraversalTransitionLocomotionHints secondToFirstHints =
 3277            TraversalTransitionLocomotionHints.RequestClimb
 3278            | (firstClimb
 3279                ? TraversalTransitionLocomotionHints.PreserveClimbAfterCompletion
 3280                : TraversalTransitionLocomotionHints.None);
 3281        builder.AddTransition(CreateTransition(
 3282            prefix,
 3283            TraversalTransitionType.Climb,
 3284            firstIndex,
 3285            TraversalMedium.Solid,
 3286            secondIndex,
 3287            TraversalMedium.Solid,
 3288            mapId,
 3289            TraversalCapability.Climb,
 3290            Fixed64.One,
 3291            firstToSecondHints));
 3292        builder.AddTransition(CreateTransition(
 3293            prefix,
 3294            TraversalTransitionType.Climb,
 3295            secondIndex,
 3296            TraversalMedium.Solid,
 3297            firstIndex,
 3298            TraversalMedium.Solid,
 3299            mapId,
 3300            TraversalCapability.Climb,
 3301            Fixed64.One,
 3302            secondToFirstHints));
 3303    }
 304
 305    private static TraversalTransitionDefinition CreateTransition(
 306        string prefix,
 307        TraversalTransitionType type,
 308        VoxelIndex source,
 309        TraversalMedium sourceMedium,
 310        VoxelIndex destination,
 311        TraversalMedium destinationMedium,
 312        string mapId,
 313        TraversalCapability capability,
 314        Fixed64 actionCost = default,
 315        TraversalTransitionLocomotionHints locomotionHints = TraversalTransitionLocomotionHints.None) =>
 28316        new(
 28317            CreateTransitionId(prefix, type, source, sourceMedium, destination, destinationMedium),
 28318            type,
 28319            source,
 28320            sourceMedium,
 28321            new NavigationCellAddress(mapId, destination),
 28322            destinationMedium,
 28323            capability,
 28324            actionCost,
 28325            locomotionHints: locomotionHints);
 326
 327    private static string CreateTransitionId(
 328        string prefix,
 329        TraversalTransitionType type,
 330        VoxelIndex source,
 331        TraversalMedium sourceMedium,
 332        VoxelIndex destination,
 333        TraversalMedium destinationMedium) =>
 28334        string.Concat(
 28335            prefix,
 28336            ":", type.ToString(), ":",
 28337            source.x.ToString(CultureInfo.InvariantCulture), "_",
 28338            source.y.ToString(CultureInfo.InvariantCulture), "_",
 28339            source.z.ToString(CultureInfo.InvariantCulture), "_", sourceMedium.ToString(), "->",
 28340            destination.x.ToString(CultureInfo.InvariantCulture), "_",
 28341            destination.y.ToString(CultureInfo.InvariantCulture), "_",
 28342            destination.z.ToString(CultureInfo.InvariantCulture), "_", destinationMedium.ToString());
 343
 344    private readonly struct ParsedTokenCell
 345    {
 346        internal NavigationTokenLegendEntry Entry { get; }
 347        internal NavigationCell Cell { get; }
 348        internal bool HasMarker { get; }
 349
 350        internal ParsedTokenCell(
 351            NavigationTokenLegendEntry entry,
 352            NavigationCell cell,
 353            bool hasMarker)
 354        {
 35355            Entry = entry;
 35356            Cell = cell;
 35357            HasMarker = hasMarker;
 35358        }
 359    }
 360}

Methods/Properties

ImportRectangular(System.String,GridForge.Configuration.GridConfiguration,System.String[0...,0...,0...],Trailblazer.Pathing.NavigationTokenLegend,System.String)
ParseCell(System.String,System.Int32,System.Int32,System.Int32,Trailblazer.Pathing.NavigationTokenLegend)
GenerateTransitions(Trailblazer.Pathing.NavigationMapBuilder,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell[0...,0...,0...],System.String,System.String)
GenerateTransitionPair(Trailblazer.Pathing.NavigationMapBuilder,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,GridForge.Spatial.VoxelIndex,GridForge.Spatial.VoxelIndex,System.String,System.String)
TryAddMediaTransitionPair(Trailblazer.Pathing.NavigationMapBuilder,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,GridForge.Spatial.VoxelIndex,GridForge.Spatial.VoxelIndex,System.String,System.String)
TrySelectBoundary(Trailblazer.Pathing.TraversalMedia,Trailblazer.Pathing.TraversalMedia,Trailblazer.Pathing.TraversalMedia,Trailblazer.TraversalMedium,GridForge.Spatial.VoxelIndex,GridForge.Spatial.VoxelIndex,System.Int32&,GridForge.Spatial.VoxelIndex&,GridForge.Spatial.VoxelIndex&,Trailblazer.TraversalMedium&)
TryAddClimbTransitionPair(Trailblazer.Pathing.NavigationMapBuilder,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,Trailblazer.Pathing.NavigationMapTokenImporter/ParsedTokenCell,GridForge.Spatial.VoxelIndex,GridForge.Spatial.VoxelIndex,System.String,System.String)
CreateTransition(System.String,Trailblazer.Pathing.TraversalTransitionType,GridForge.Spatial.VoxelIndex,Trailblazer.TraversalMedium,GridForge.Spatial.VoxelIndex,Trailblazer.TraversalMedium,System.String,Trailblazer.Pathing.TraversalCapability,FixedMathSharp.Fixed64,Trailblazer.Pathing.TraversalTransitionLocomotionHints)
CreateTransitionId(System.String,Trailblazer.Pathing.TraversalTransitionType,GridForge.Spatial.VoxelIndex,Trailblazer.TraversalMedium,GridForge.Spatial.VoxelIndex,Trailblazer.TraversalMedium)
.ctor(Trailblazer.Pathing.NavigationTokenLegendEntry,Trailblazer.Pathing.NavigationCell,System.Boolean)