| | | 1 | | using FixedMathSharp; |
| | | 2 | | using System; |
| | | 3 | | |
| | | 4 | | namespace Trailblazer.Pathing; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Builds chart data and explicit transitions from tokenized traversable-state authoring input. |
| | | 8 | | /// </summary> |
| | | 9 | | public sealed class TraversalAuthoringMap |
| | | 10 | | { |
| | | 11 | | |
| | | 12 | | /// <summary> |
| | | 13 | | /// Creates a new traversal authoring map with the specified parameters. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <param name="chartName">The name of the chart.</param> |
| | | 16 | | /// <param name="sourceMap">The source map containing tokenized traversable-state data.</param> |
| | | 17 | | /// <param name="minBounds">The minimum bounds of the authored map in world space.</param> |
| | | 18 | | /// <param name="interval">The interval between voxels in the authored map.</param> |
| | | 19 | | /// <param name="legend">The legend used to interpret tokens in the source map.</param> |
| | | 20 | | /// <param name="transitionIdPrefix">The prefix applied to generated transition IDs.</param> |
| | | 21 | | /// <exception cref="ArgumentException"></exception> |
| | | 22 | | /// <exception cref="ArgumentNullException"></exception> |
| | 44 | 23 | | public TraversalAuthoringMap( |
| | 44 | 24 | | string chartName, |
| | 44 | 25 | | string[,,] sourceMap, |
| | 44 | 26 | | Vector3d minBounds, |
| | 44 | 27 | | Fixed64 interval, |
| | 44 | 28 | | TraversalLegend? legend = null, |
| | 44 | 29 | | string? transitionIdPrefix = null) |
| | | 30 | | { |
| | 44 | 31 | | if (string.IsNullOrWhiteSpace(chartName)) |
| | 0 | 32 | | throw new ArgumentException("Chart name cannot be null or whitespace.", nameof(chartName)); |
| | | 33 | | |
| | 44 | 34 | | ChartName = chartName; |
| | 44 | 35 | | SourceMap = sourceMap ?? throw new ArgumentNullException(nameof(sourceMap)); |
| | 44 | 36 | | MinBounds = minBounds; |
| | 44 | 37 | | Interval = interval; |
| | 44 | 38 | | Legend = legend ?? TraversalLegend.CreateBuiltIn(); |
| | 44 | 39 | | TransitionIdPrefix = string.IsNullOrWhiteSpace(transitionIdPrefix) |
| | 44 | 40 | | ? chartName |
| | 44 | 41 | | : transitionIdPrefix; |
| | 44 | 42 | | } |
| | | 43 | | |
| | | 44 | | /// <summary> |
| | | 45 | | /// The chart name used for the built chart. |
| | | 46 | | /// </summary> |
| | | 47 | | public string ChartName { get; } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// The raw token source map. |
| | | 51 | | /// </summary> |
| | | 52 | | public string[,,] SourceMap { get; } |
| | | 53 | | |
| | | 54 | | /// <summary> |
| | | 55 | | /// The world-space minimum bounds of the authored map. |
| | | 56 | | /// </summary> |
| | | 57 | | public Vector3d MinBounds { get; } |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// The authored voxel interval. |
| | | 61 | | /// </summary> |
| | | 62 | | public Fixed64 Interval { get; } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// The token legend used during parsing. |
| | | 66 | | /// </summary> |
| | | 67 | | public TraversalLegend Legend { get; } |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Prefix applied to generated transition ids. |
| | | 71 | | /// </summary> |
| | | 72 | | public string TransitionIdPrefix { get; } |
| | | 73 | | |
| | | 74 | | /// <summary> |
| | | 75 | | /// Parses the source map and builds a chart and set of explicit transitions according to the legend and authoring r |
| | | 76 | | /// </summary> |
| | | 77 | | /// <returns>A build result containing the generated chart and transitions.</returns> |
| | | 78 | | public TraversalBuildResult Build() |
| | | 79 | | { |
| | 43 | 80 | | int sizeY = SourceMap.GetLength(0); |
| | 43 | 81 | | int sizeX = SourceMap.GetLength(1); |
| | 43 | 82 | | int sizeZ = SourceMap.GetLength(2); |
| | | 83 | | |
| | 43 | 84 | | var parsedCells = new ParsedTraversalCell[sizeY, sizeX, sizeZ]; |
| | 43 | 85 | | var chartCells = new NavigationChartCell[sizeY, sizeX, sizeZ]; |
| | | 86 | | |
| | 170 | 87 | | for (int y = 0; y < sizeY; y++) |
| | 274 | 88 | | for (int x = 0; x < sizeX; x++) |
| | 424 | 89 | | for (int z = 0; z < sizeZ; z++) |
| | | 90 | | { |
| | 121 | 91 | | ParsedTraversalCell parsedCell = ParseCell(SourceMap[y, x, z], y, x, z); |
| | 117 | 92 | | parsedCells[y, x, z] = parsedCell; |
| | 117 | 93 | | chartCells[y, x, z] = BuildChartCell(parsedCell); |
| | | 94 | | } |
| | | 95 | | |
| | 39 | 96 | | var chart = NavigationChart.From3D(ChartName, chartCells, MinBounds, Interval); |
| | 39 | 97 | | TraversalTransition[] generatedTransitions = |
| | 39 | 98 | | GeneratedTraversalTransitionBuilder.BuildTransitions(chart, TransitionIdPrefix); |
| | 39 | 99 | | return new TraversalBuildResult(chart, generatedTransitions, TransitionIdPrefix); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | private static NavigationChartCell BuildChartCell(ParsedTraversalCell parsedCell) |
| | | 103 | | { |
| | 117 | 104 | | NavigationChartCell chartCell = parsedCell.Entry.ChartCell; |
| | 117 | 105 | | int costModifier = parsedCell.PathCostModifier; |
| | 117 | 106 | | NavigationChartCellFlags flags = chartCell.Flags; |
| | | 107 | | |
| | 117 | 108 | | if (parsedCell.HasTransitionMarker |
| | 117 | 109 | | && (flags & NavigationChartCellFlags.ClimbSurfaceHint) != 0) |
| | | 110 | | { |
| | 9 | 111 | | flags |= NavigationChartCellFlags.ClimbTransitionHint; |
| | | 112 | | } |
| | | 113 | | |
| | 117 | 114 | | if (!parsedCell.HasTransitionMarker || !parsedCell.CanGenerateTransition) |
| | | 115 | | { |
| | 79 | 116 | | if (costModifier == 0) |
| | | 117 | | { |
| | 73 | 118 | | if (flags == chartCell.Flags) |
| | 66 | 119 | | return chartCell; |
| | | 120 | | |
| | 7 | 121 | | return new NavigationChartCell( |
| | 7 | 122 | | chartCell.TraversalKinds, |
| | 7 | 123 | | chartCell.PathCostModifier, |
| | 7 | 124 | | flags, |
| | 7 | 125 | | chartCell.GeneratedTransitionMedia); |
| | | 126 | | } |
| | | 127 | | |
| | 6 | 128 | | return new NavigationChartCell( |
| | 6 | 129 | | chartCell.TraversalKinds, |
| | 6 | 130 | | costModifier, |
| | 6 | 131 | | flags, |
| | 6 | 132 | | chartCell.GeneratedTransitionMedia); |
| | | 133 | | } |
| | | 134 | | |
| | 38 | 135 | | if (chartCell.HasSolid) |
| | | 136 | | { |
| | 22 | 137 | | flags |= NavigationChartCellFlags.TransitionSourceHint |
| | 22 | 138 | | | NavigationChartCellFlags.TransitionDestinationHint; |
| | | 139 | | } |
| | | 140 | | |
| | 38 | 141 | | return new NavigationChartCell( |
| | 38 | 142 | | chartCell.TraversalKinds, |
| | 38 | 143 | | costModifier != 0 ? costModifier : chartCell.PathCostModifier, |
| | 38 | 144 | | flags, |
| | 38 | 145 | | parsedCell.TransitionMedia); |
| | | 146 | | } |
| | | 147 | | |
| | | 148 | | private ParsedTraversalCell ParseCell(string rawToken, int y, int x, int z) |
| | | 149 | | { |
| | 121 | 150 | | string normalizedToken = rawToken?.Trim() ?? string.Empty; |
| | 121 | 151 | | bool hasTransitionMarker = false; |
| | 121 | 152 | | if (normalizedToken.Length > 0) |
| | | 153 | | { |
| | 93 | 154 | | int markerIndex = normalizedToken.IndexOf('!'); |
| | 93 | 155 | | if (markerIndex >= 0) |
| | | 156 | | { |
| | 49 | 157 | | if (markerIndex != normalizedToken.Length - 1 |
| | 49 | 158 | | || normalizedToken.LastIndexOf('!') != markerIndex) |
| | | 159 | | { |
| | 2 | 160 | | throw new ArgumentException( |
| | 2 | 161 | | $"Invalid token '{normalizedToken}' at [{y}, {x}, {z}]. Only a single trailing '!' marker is sup |
| | | 162 | | } |
| | | 163 | | |
| | 47 | 164 | | hasTransitionMarker = true; |
| | 47 | 165 | | normalizedToken = normalizedToken[..^1].TrimEnd(); |
| | 47 | 166 | | if (string.IsNullOrEmpty(normalizedToken)) |
| | | 167 | | { |
| | 1 | 168 | | throw new ArgumentException( |
| | 1 | 169 | | $"Invalid token '{rawToken}' at [{y}, {x}, {z}]. Transition markers require a base token."); |
| | | 170 | | } |
| | | 171 | | } |
| | | 172 | | } |
| | | 173 | | |
| | | 174 | | // Parse an optional inline path cost modifier suffix: <token>_<int> (e.g. "S_60", "SL_45"). |
| | | 175 | | // The suffix is extracted after transition-marker stripping so "S_60!" is also valid. |
| | 118 | 176 | | int pathCostModifier = 0; |
| | 118 | 177 | | int underscoreIndex = normalizedToken.LastIndexOf('_'); |
| | 118 | 178 | | if (underscoreIndex >= 0) |
| | | 179 | | { |
| | 10 | 180 | | string costPart = normalizedToken[(underscoreIndex + 1)..]; |
| | 10 | 181 | | if (int.TryParse(costPart, out int parsedCost)) |
| | | 182 | | { |
| | 10 | 183 | | pathCostModifier = parsedCost; |
| | 10 | 184 | | normalizedToken = normalizedToken[..underscoreIndex].TrimEnd(); |
| | | 185 | | } |
| | | 186 | | } |
| | | 187 | | |
| | 118 | 188 | | if (!Legend.TryGetEntry(normalizedToken, out TraversalLegendEntry entry)) |
| | | 189 | | { |
| | 0 | 190 | | throw new ArgumentException( |
| | 0 | 191 | | $"Unknown traversable-state token '{rawToken}' at [{y}, {x}, {z}]."); |
| | | 192 | | } |
| | | 193 | | |
| | 118 | 194 | | if (hasTransitionMarker |
| | 118 | 195 | | && !entry.HasTransitionMedia |
| | 118 | 196 | | && (entry.ChartCell.Flags & NavigationChartCellFlags.ClimbSurfaceHint) == 0) |
| | | 197 | | { |
| | 1 | 198 | | throw new ArgumentException( |
| | 1 | 199 | | $"Token '{rawToken}' at [{y}, {x}, {z}] cannot be marked for transition generation."); |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | // Ignore cost modifiers on skip cells; they contribute no traversal data. |
| | 117 | 203 | | if (!entry.ChartCell.HasTraversalData) |
| | 34 | 204 | | pathCostModifier = 0; |
| | | 205 | | |
| | 117 | 206 | | return new ParsedTraversalCell(entry, hasTransitionMarker, pathCostModifier); |
| | | 207 | | } |
| | | 208 | | } |