< Summary

Line coverage
97%
Covered lines: 547
Uncovered lines: 16
Coverable lines: 563
Total lines: 1404
Line coverage: 97.1%
Branch coverage
82%
Covered branches: 207
Total branches: 250
Branch coverage: 82.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%11100%
File 1: .ctor()100%1191.66%
File 1: get_Destination()100%11100%
File 1: get_TargetDirection()100%11100%
File 1: get_LastTargetDirection()100%11100%
File 1: get_CurrentRequest()100%11100%
File 1: get_TrailGuide()100%11100%
File 1: get_ShouldMove()100%11100%
File 1: get_IsStuck()100%11100%
File 1: get_HasLineOfSightPath()100%11100%
File 1: get_CurrentRouteRequestsClimbIntent()100%11100%
File 1: get_CurrentRouteTopologyVersion()100%11100%
File 1: get_DistanceToTarget()100%11100%
File 1: get_HasTrailGuide()100%22100%
File 1: get_IsAtDestination()100%11100%
File 1: get_StoppedFrameCount()100%11100%
File 1: get_CanAutoStop()100%11100%
File 1: get_Context()100%210%
File 1: CreateNew(...)100%11100%
File 1: .ctor(...)100%11100%
File 1: BindContext(...)75%4485.71%
File 1: get_MovementGroupID()100%11100%
File 1: set_MovementGroupID(...)100%11100%
File 1: get_GroupIndex()100%11100%
File 1: set_GroupIndex(...)100%11100%
File 1: get_IsInGroup()100%11100%
File 2: ComputeCombinedSteering(...)81.81%2222100%
File 2: CacheOwner(...)100%22100%
File 2: UpdateMovementGroupState(...)83.33%66100%
File 2: GetActiveStopMultiplier()100%22100%
File 2: IsGroupNeighbor(...)100%11100%
File 2: UsesVolumeGuidance()100%11100%
File 2: PublishRouteTopology(...)100%88100%
File 3: IsDestinationInSight(...)100%11100%
File 3: IsVolumeDestinationInSight(...)100%11100%
File 4: ApplyPathRequest(...)75%121292.3%
File 4: PauseAutoStop()100%11100%
File 4: SetTrailGuide(...)100%11100%
File 4: AddToMovementGroup(...)83.33%66100%
File 4: LeaveMovementGroup()100%11100%
File 4: PrewarmMovementGroup(...)83.33%66100%
File 5: RecordData(...)92.3%2626100%
File 5: ReleaseTrailGuide(...)66.66%66100%
File 5: ResetMovementGroupSession()100%11100%
File 6: OnInitialize(...)100%11100%
File 6: UpdateOwnerRadius(...)100%11100%
File 6: Reset()100%11100%
File 6: ResolveVoxelSize()25%7440%
File 6: ResolveContext()33.33%6680%
File 6: get_MovementGroups()100%11100%
File 6: RemoveMovementGroupSession()100%11100%
File 6: get_StuckFrameThresholdForContext()100%11100%
File 6: get_AutoPauseStopTimeForContext()100%11100%
File 6: ResolveFrameRate()25%7440%
File 6: GetHeading(...)93.75%1616100%
File 6: ValidateMovementPath(...)87.5%2424100%
File 6: FindTargetDirection(...)90%1010100%
File 6: ShouldAdvanceToNextWaypoint()75%44100%
File 6: CheckStuckStatus(...)100%1010100%
File 6: FinalizeIdleHeading(...)100%22100%
File 6: TryEnsureCurrentRequest(...)100%22100%
File 6: TryPrepareMovementPathForHeading(...)100%1212100%
File 6: RefreshLineOfSightState(...)87.5%88100%
File 6: HandleInvalidPath(...)50%44100%
File 6: UpdateTargetDirection(...)100%11100%
File 6: ShouldArriveWithoutTrailGuide()100%44100%
File 6: UpdateTrailGuideProgress(...)87.5%88100%
File 6: FinalizeHeadingFrame()100%22100%
File 6: ResetStuckStatus()100%11100%
File 6: TryRecoverFromStuck(...)75%4487.5%
File 6: TryApplyFallbackDirection(...)100%44100%
File 6: PreparePathRetry()100%11100%
File 6: DeclareHardStuck()50%22100%
File 6: DisposeCurrentTrailGuide()16.66%14640%
File 6: SetDeceleration(...)83.33%66100%
File 6: Arrive()100%22100%
File 6: StopMove()100%44100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.cs

#LineLine coverage
 1using Chronicler;
 2using FixedMathSharp;
 3using GridForge.Grids;
 4using SwiftCollections;
 5using System;
 6using System.Runtime.CompilerServices;
 7using Trailblazer.Navigation.MovementGroups;
 8using Trailblazer.Pathing;
 9
 10namespace Trailblazer.Navigation.Steering;
 11
 12/// <summary>
 13/// Handles agent steering and path navigation logic by coordinating pathfinding, movement,
 14/// and group behaviors within a lockstep simulation. Supports both direct line-of-sight travel
 15/// and guided path traversal using IGuide implementations like AStar or FlowField.
 16/// </summary>
 17public partial class NavSteering : IRecordable
 18{
 19    #region Constants & Defaults
 20
 21    /// <summary>
 22    /// Default range to scan for other agents when calculating steering behaviors.
 23    /// </summary>
 124    protected static readonly Fixed64 DefaultGroupFactor = (Fixed64)10;
 25
 26    /// <summary>
 27    /// Default padding radius used to maintain space between nearby agents.
 28    /// </summary>
 129    protected static readonly Fixed64 DefaultAvoidFactor = (Fixed64)3;
 30
 31    /// <summary>
 32    /// Default weights used for group-based steering calculations (separation, alignment, cohesion).
 33    /// </summary>
 134    protected static readonly GroupBehaviorWeights DefaultBehaviorWeights = new()
 135    {
 136        Separation = (Fixed64)2,
 137        Alignment = Fixed64.Half,
 138        Cohesion = (Fixed64)0.2f,
 139        Avoidance = Fixed64.One
 140    };
 41
 42    /// <summary>
 43    /// Default multiplier used to determine proximity tolerance when stopping at a destination.
 44    /// </summary>
 145    public static readonly Fixed64 DefaultDirectStop = Fixed64.FromRaw(0x40000000L); // 0.25f;
 46
 47    /// <summary>
 48    /// Number of frames between pathfinding LOS rechecks.
 49    /// </summary>
 50    protected const int DefaultPathRecheckCooldown = 16;
 51
 52    /// <summary>
 53    /// Maximum number of repath attempts before declaring the agent fully stuck.
 54    /// </summary>
 55    protected const int StuckRepathTries = 4;
 56
 57    /// <summary>
 58    /// Default braking factor applied when decelerating or stopping motion.
 59    /// </summary>
 160    public static readonly Fixed64 DefaultBrakingPower = (Fixed64)0.15d;
 61
 62    /// <summary>
 63    /// Group fallback stop tolerance used when a formation breaks apart near the goal.
 64    /// </summary>
 165    protected static readonly Fixed64 DefaultGroupIndividualStop = Fixed64.One;
 66
 67    #endregion
 68
 69    #region Fields
 70
 71    /// <summary>
 72    /// The final destination this agent is attempting to reach.
 73    /// </summary>
 74    protected Vector3d _destination;
 75
 76    /// <summary>
 77    /// Gets the current target direction as a three-dimensional vector.
 78    /// </summary>
 79    protected Vector3d _targetDirection;
 80
 81    /// <summary>
 82    /// Gets the direction vector of the most recent target interaction.
 83    /// </summary>
 84    protected Vector3d _lastTargetDirection;
 85
 86    /// <summary>
 87    /// Whether the object is following a path or guide to the destination.
 88    /// </summary>
 89    protected bool _shouldMove;
 90
 91    /// <summary>
 92    /// Whether the agent has become stuck and exhausted repathing attempts.
 93    /// </summary>
 94    protected bool _isStuck;
 95
 96    /// <summary>
 97    /// True if the agent can reach the destination without requiring a path.
 98    /// </summary>
 99    protected bool _hasLineOfSightPath;
 100
 101    /// <summary>
 102    /// Whether the currently resolved guide-backed route requires climb intent to remain engaged.
 103    /// </summary>
 104    protected bool _currentRouteRequestsClimbIntent;
 105
 106    /// <summary>
 107    /// Version token that changes when the resolved route state relevant to guided climb intent changes.
 108    /// </summary>
 109    protected int _currentRouteTopologyVersion;
 110
 111    /// <summary>
 112    /// Has this unit arrived at destination?
 113    /// </summary>
 114    protected bool _isAtDestination;
 115
 116    /// <summary>
 117    /// Number of consecutive frames where movement failed and deceleration is occurring.
 118    /// </summary>
 119    protected int _stoppedFrameCount;
 120
 121    #endregion
 122
 123    #region Runtime State - Pathfinding
 124
 125    /// <summary>
 126    /// Disable if a unit never needs voxel-guide validation or repathing.
 127    /// </summary>
 199128    public bool CanPathfind = true;
 129
 130    /// <inheritdoc cref="_destination"/>
 174131    public Vector3d Destination => _destination;
 132
 133    private Vector3d _requestedDestination;
 134
 135    private Fixed64 _lastUnitSize;
 136
 137    /// <inheritdoc cref="DefaultPathRecheckCooldown"/>
 199138    public int PathRecheckCooldownFrames = DefaultPathRecheckCooldown;
 139
 140    /// <inheritdoc cref="_targetDirection"/>
 483141    public Vector3d TargetDirection => _targetDirection;
 142
 143    /// <inheritdoc cref="_lastTargetDirection"/>
 29144    public Vector3d LastTargetDirection => _lastTargetDirection;
 145
 146    /// <summary>
 147    /// The pathfinding configuration used for the current movement request, including size, and type.
 148    /// </summary>
 149    private IPathRequest? _currentRequest;
 150
 151    /// <inheritdoc cref="_currentRequest"/>
 222152    public IPathRequest? CurrentRequest => _currentRequest;
 153
 154    /// <summary>
 155    /// Current guide used to compute the desired path or flow.
 156    /// </summary>
 157    private IGuide? _trailGuide;
 158
 159    /// <inheritdoc cref="_trailGuide"/>
 87160    public IGuide? TrailGuide => _trailGuide;
 161
 162    /// <inheritdoc cref="_shouldMove"/>
 428163    public bool ShouldMove => _shouldMove;
 164
 165    /// <inheritdoc cref="_isStuck"/>
 63166    public bool IsStuck => _isStuck;
 167
 168    /// <inheritdoc cref="_hasLineOfSightPath"/>
 414169    public bool HasLineOfSightPath => _hasLineOfSightPath;
 170
 171    /// <inheritdoc cref="_currentRouteRequestsClimbIntent"/>
 42172    public bool CurrentRouteRequestsClimbIntent => _currentRouteRequestsClimbIntent;
 173
 174    /// <inheritdoc cref="_currentRouteTopologyVersion"/>
 126175    public int CurrentRouteTopologyVersion => _currentRouteTopologyVersion;
 176
 177    /// <summary>
 178    /// Current pathfinding search status.
 179    /// </summary>
 180    protected bool _shouldRequestPathThisFrame;
 181
 182    /// <summary>
 183    /// Represents the cooldown period, in milliseconds, before the next path check can be performed.
 184    /// </summary>
 185    protected int _pathCheckCooldown;
 186
 187    private bool _currentRouteHasResolvedTopology;
 188
 189    private bool _currentRouteUsesGuideTopology;
 190
 191    /// <summary>
 192    /// How far we move each update
 193    /// </summary>
 194    protected Fixed64 _distanceToTarget;
 195
 196    /// <inheritdoc cref="_distanceToTarget"/>
 68197    public Fixed64 DistanceToTarget => _distanceToTarget;
 198
 199    /// <summary>
 200    /// How far away the agent stops from the target
 201    /// </summary>
 202    private Fixed64 _closingDistance;
 203
 204    /// <summary>
 205    /// Indicates whether the agent is actively following a guide path with queued waypoints.
 206    /// </summary>
 242207    public bool HasTrailGuide => !HasLineOfSightPath && _trailGuide != null;
 208
 209    /// <inheritdoc cref="_isAtDestination"/>
 125210    public bool IsAtDestination => _isAtDestination;
 211
 212    #endregion
 213
 214    #region Runtime State - Steering & Motion
 215
 216    /// <summary>
 217    /// Whether this agent can currently move.
 218    /// </summary>
 199219    public bool CanMove = true;
 220
 221    /// <inheritdoc cref="_stoppedFrameCount"/>
 12222    public int StoppedFrameCount => _stoppedFrameCount;
 223
 224    /// <summary>
 225    /// Internal cooldown before the agent can automatically stop again (used for bursty movement).
 226    /// </summary>
 227    protected int _autoStopFrameCount;
 228
 229    /// <summary>
 230    /// Indicates whether the agent is currently eligible for automatic stopping logic.
 231    /// </summary>
 114232    public bool CanAutoStop => _autoStopFrameCount <= 0;
 233
 234    /// <summary>
 235    /// Number of attempts to repath after getting stuck.
 236    /// </summary>
 237    protected int _repathTries;
 238
 239    /// <summary>
 240    /// Number of frames the agent has failed movement checks (used for stuck detection).
 241    /// </summary>
 242    protected int _stuckFrameCount;
 243
 244    /// <summary>
 245    /// Multiplier used to determine how close the agent must be to its target before stopping.
 246    /// </summary>
 199247    public Fixed64 StopMultiplier = DefaultDirectStop;
 248
 249    /// <summary>
 250    /// How far to look for group neighbors (separation/alignment/cohesion).
 251    /// </summary>
 199252    public Fixed64 GroupFactor = DefaultGroupFactor;
 253
 254    /// <summary>
 255    /// How far to look for obstacles to avoid.
 256    /// </summary>
 199257    public Fixed64 AvoidFactor = DefaultAvoidFactor;
 258
 259    /// <summary>
 260    /// Weights for separating, aligning, and cohesion in group behavior.
 261    /// Avoidance weight is baked in here as well.
 262    /// </summary>
 199263    public GroupBehaviorWeights BehaviorWeights = DefaultBehaviorWeights;
 264
 265    /// <summary>
 266    /// Friction-based deceleration rate used when slowing down on ground surfaces.
 267    /// </summary>
 199268    public Fixed64 BrakingPower = DefaultBrakingPower;
 269
 270    private Fixed64 _agentRadius;
 271
 199272    private readonly MovementGroupSession _movementGroupSession = new();
 273
 199274    private readonly SwiftList<ISteer> _nearbySteerAgents = new();
 275
 199276    private readonly GridScanScratch _scanScratch = new();
 277
 278    private MovementGroupTravelMode _movementGroupMode;
 279
 280    private TrailblazerWorldContext? _context;
 281
 282    #endregion
 283
 284    #region Events
 285
 286    /// <summary>
 287    /// Container for delegate events that fire on pathfinding state changes (start, stop, arrive).
 288    /// </summary>
 289    public NavSteeringEvents Events { get; protected set; } = new();
 290
 291    /// <summary>
 292    /// Gets the world context this steering controller is bound to, when explicitly bound.
 293    /// </summary>
 0294    public TrailblazerWorldContext? Context => _context;
 295
 296    #endregion
 297
 298    #region Constructors
 299
 300    /// <summary>
 301    /// Creates a new <see cref="NavSteering"/> instance bound to a world context.
 302    /// </summary>
 148303    public static NavSteering CreateNew(TrailblazerWorldContext context, Fixed64 radius) => new(context, radius);
 304
 0305    private NavSteering() { }
 306
 307    /// <summary>
 308    /// Initializes a new context-bound <see cref="NavSteering"/> instance.
 309    /// </summary>
 199310    public NavSteering(TrailblazerWorldContext context, Fixed64 radius)
 311    {
 199312        BindContext(context);
 199313        OnInitialize(radius);
 199314    }
 315
 316    /// <summary>
 317    /// Binds this steering controller to a world context.
 318    /// </summary>
 319    public void BindContext(TrailblazerWorldContext context)
 320    {
 241321        PathRequestContextResolver.ThrowIfUnusable(context);
 322
 241323        if (ReferenceEquals(_context, context))
 42324            return;
 325
 199326        if (_context != null)
 0327            _context.Navigation.MovementGroups.Remove(_movementGroupSession);
 328
 199329        _context = context;
 199330    }
 331
 332    #endregion
 333
 334    #region Group Properties
 335
 336    /// <summary>
 337    /// Gets or sets the unique identifier for the movement group associated with the current session.
 338    /// </summary>
 339    public int MovementGroupID
 340    {
 792341        get => _movementGroupSession.GroupId;
 373342        set => _movementGroupSession.GroupId = value;
 343    }
 344
 345    /// <summary>
 346    /// Gets the index of the group associated with the current movement session.
 347    /// </summary>
 348    public int GroupIndex
 349    {
 1350        get => _movementGroupSession.GroupIndex;
 328351        protected set => _movementGroupSession.GroupIndex = value;
 352    }
 353
 354    /// <summary>
 355    /// Gets a value indicating whether the item is assigned to a movement group.
 356    /// </summary>
 725357    public bool IsInGroup => MovementGroupID != -1;
 358
 359    #endregion
 360
 361
 362
 363
 364
 365
 366}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.Groups.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using System;
 4using System.Runtime.CompilerServices;
 5using Trailblazer.Navigation.MovementGroups;
 6using Trailblazer.Pathing;
 7
 8namespace Trailblazer.Navigation.Steering;
 9
 10public partial class NavSteering
 11{
 12    #region Steering Behaviors (Group & Avoidance)
 13
 14    /// <summary>
 15    /// Computes a combined steering vector—
 16    /// Separation, Alignment, Cohesion, plus single-nearest obstacle avoidance.
 17    /// </summary>
 18    public Vector3d ComputeCombinedSteering(
 19        Vector3d position,
 20        Vector3d velocity,
 21        Fixed64 speed,
 22        Fixed64 radius,
 23        Guid id)
 24    {
 36725        if (speed <= Fixed64.Zero)
 9126            return Vector3d.Zero;
 27
 27628        TrailblazerWorldContext context = ResolveContext();
 27629        int currentFrame = context.FrameCount;
 30
 31        // we need to see everybody who might influence us—either for group or for avoidance
 27632        Fixed64 groupRadius = radius * GroupFactor;
 27633        Fixed64 invGR = Fixed64.One / groupRadius;
 27634        Fixed64 avoidRadius = radius * AvoidFactor;
 27635        Fixed64 scanRadius = FixedMath.Max(groupRadius, avoidRadius);
 27636        Fixed64 groupRadiusSq = groupRadius * groupRadius;
 37
 38        // Accumulators
 27639        Vector3d separation = Vector3d.Zero;
 27640        Vector3d alignment = Vector3d.Zero;
 27641        Vector3d cohesionCM = Vector3d.Zero;
 27642        int groupCount = 0;
 43
 27644        ISteer? closest = null;
 27645        Fixed64 closestDistSq = avoidRadius * avoidRadius;
 46
 27647        GridScanManager.ScanRadiusInto<ISteer>(
 27648            context.World,
 27649            position,
 27650            scanRadius,
 27651            _nearbySteerAgents,
 27652            _scanScratch);
 53
 57654        for (int i = 0; i < _nearbySteerAgents.Count; i++)
 55        {
 1256            ISteer other = _nearbySteerAgents[i];
 1257            if (other.Radius <= Fixed64.Zero)
 58                continue;
 59
 1260            if (other.GlobalId == id)
 61                continue;
 62
 863            Vector3d offset = other.Position - position;
 864            Fixed64 distSq = offset.SqrMagnitude;
 865            if (distSq <= Fixed64.Epsilon)
 66                continue;
 67
 68            // Group behaviors
 869            if (IsGroupNeighbor(other.GlobalId, currentFrame) && distSq < groupRadiusSq)
 70            {
 371                groupCount++;
 372                Fixed64 d = FixedMath.Sqrt(distSq);
 373                Fixed64 invD = Fixed64.One / d;
 374                Vector3d norm = offset * invD;  // offset.Normal
 75                // stronger separation the closer they are
 376                Fixed64 push = (groupRadius - d) * invGR;
 377                separation -= norm * push;
 378                alignment += other.Velocity.Normal;
 379                cohesionCM += other.Position;
 80            }
 81
 82            // Track nearest for avoidance
 883            if (distSq < closestDistSq)
 84            {
 885                closestDistSq = distSq;
 886                closest = other;
 87            }
 88        }
 89
 27690        Vector3d groupForce = Vector3d.Zero;
 91        // Finalize group forces
 27692        if (groupCount > 0)
 93        {
 394            Vector3d sep = separation * BehaviorWeights.Separation;
 395            Vector3d align = (alignment / groupCount).Normal * BehaviorWeights.Alignment;
 396            Vector3d coh = ((cohesionCM / groupCount - position).Normal) * BehaviorWeights.Cohesion;
 397            groupForce = sep + align + coh;
 98        }
 99
 100        // Compute avoidance
 276101        Vector3d avoidance = Vector3d.Zero;
 276102        if (closest != null)
 103        {
 8104            Vector3d dir = closest.Position - position;
 105            // pick left/right dodge
 8106            bool dodgeLeft = Vector3d.Dot(velocity, dir) >= Fixed64.Zero;
 8107            Vector3d perp = dodgeLeft
 8108                ? new(-dir.z, Fixed64.Zero, dir.x)
 8109                : new(dir.z, Fixed64.Zero, -dir.x);
 110
 111            // prioritize evasive action when facing direct collision(dot ~ ±1),
 112            // and de-emphasize near misses(dot ~0)
 8113            Fixed64 dynamicAvoidWeight = Vector3d.Dot(velocity.Normal, dir.Normal);
 8114            Fixed64 totalAvoidWeight = BehaviorWeights.Avoidance * dynamicAvoidWeight;
 8115            avoidance = perp.Normal
 8116                * ((radius + closest.Radius) / FixedMath.Sqrt(closestDistSq))
 8117                * totalAvoidWeight;
 118        }
 119
 276120        return groupForce + avoidance;
 121    }
 122
 123    #endregion
 124
 125    #region Movement Groups
 126
 127    private void CacheOwner(ISteer vessel)
 128    {
 309129        if (IsInGroup)
 48130            MovementGroups.CacheOwner(_movementGroupSession, vessel.GlobalId);
 309131    }
 132
 133    private void UpdateMovementGroupState(Vector3d position, bool resetFormationOffset = false)
 134    {
 204135        var target = new MovementGroupTarget(
 204136            travelMode: IsInGroup ? MovementGroupTravelMode.Individual : MovementGroupTravelMode.None,
 204137            destination: _requestedDestination);
 138
 204139        if (IsInGroup && _currentRequest != null)
 140        {
 91141            target = MovementGroups.UpdateTarget(
 91142                _movementGroupSession,
 91143                _requestedDestination,
 91144                position,
 91145                _agentRadius,
 91146                resetFormationOffset);
 147        }
 148
 204149        _destination = target.Destination;
 204150        _movementGroupMode = target.TravelMode;
 204151    }
 152
 153    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 154    private Fixed64 GetActiveStopMultiplier() =>
 47155        _movementGroupMode == MovementGroupTravelMode.GroupIndividual
 47156            ? DefaultGroupIndividualStop
 47157            : StopMultiplier;
 158
 159    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 160    private bool IsGroupNeighbor(Guid otherId, int currentFrame)
 8161        => MovementGroups.IsNeighbor(_movementGroupSession, otherId, _requestedDestination, currentFrame);
 162
 163    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 103164    private bool UsesVolumeGuidance() => _currentRequest is VolumePathRequest;
 165
 166    private void PublishRouteTopology(
 167        bool hasResolvedTopology,
 168        bool usesGuideTopology,
 169        bool requestsClimbIntent,
 170        bool force = false)
 171    {
 225172        if (!force
 225173            && _currentRouteHasResolvedTopology == hasResolvedTopology
 225174            && _currentRouteUsesGuideTopology == usesGuideTopology
 225175            && _currentRouteRequestsClimbIntent == requestsClimbIntent)
 176        {
 32177            return;
 178        }
 179
 193180        _currentRouteHasResolvedTopology = hasResolvedTopology;
 193181        _currentRouteUsesGuideTopology = usesGuideTopology;
 193182        _currentRouteRequestsClimbIntent = requestsClimbIntent;
 183        unchecked
 184        {
 193185            _currentRouteTopologyVersion++;
 186        }
 193187    }
 188
 189    #endregion
 190}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.LineOfSight.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using Trailblazer.Pathing;
 4
 5namespace Trailblazer.Navigation.Steering;
 6
 7public partial class NavSteering
 8{
 9    #region Line-of-Sight & Reachability
 10
 11    /// <summary>
 12    /// Whether the destination is visible and reachable inside the supplied world context.
 13    /// </summary>
 14    public static bool IsDestinationInSight(
 15        TrailblazerWorldContext context,
 16        Vector3d position,
 17        Vector3d destination,
 18        Fixed64 unitSize,
 19        bool allowUnwalkableEndpoints)
 20    {
 8521        return !context.Pathing.NeedsPath(position, destination, unitSize, allowUnwalkableEndpoints);
 22    }
 23
 24    /// <summary>
 25    /// Whether the destination is currently visible and reachable for raw-volume travel in the supplied context.
 26    /// </summary>
 27    public static bool IsVolumeDestinationInSight(
 28        TrailblazerWorldContext context,
 29        Vector3d position,
 30        Vector3d destination,
 31        Fixed64 unitSize,
 32        bool allowUnwalkableEndpoints,
 33        TraversalMedium medium = TraversalMedium.Gas,
 34        Voxel? startNode = null,
 35        Voxel? endNode = null)
 36    {
 1537        return VolumeVoxelFinder.IsDirectPathClear(
 1538            context,
 1539            position,
 1540            destination,
 1541            unitSize,
 1542            allowUnwalkableEndpoints,
 1543            medium,
 1544            startNode,
 1545            endNode);
 46    }
 47
 48    #endregion
 49}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.Requests.cs

#LineLine coverage
 1using FixedMathSharp;
 2using System;
 3using System.Runtime.CompilerServices;
 4using SwiftCollections;
 5using Trailblazer.Navigation.MovementGroups;
 6using Trailblazer.Pathing;
 7
 8namespace Trailblazer.Navigation.Steering;
 9
 10public partial class NavSteering
 11{
 12    #region Public Interface
 13
 14    /// <summary>
 15    /// Starts or replaces the active steering request.
 16    /// </summary>
 17    /// <param name="pathRequest">The movement request that defines the desired origin and destination.</param>
 18    /// <param name="groupId">Optional shared group identifier used to preserve formation offsets between nearby members
 19    public virtual void ApplyPathRequest(IPathRequest? pathRequest, int groupId = -1)
 20    {
 21        // assume the object is being controlled
 10222        if (pathRequest == null || !pathRequest.HasValidEndpoints)
 23        {
 124            TrailblazerLogger.Channel.Warn($"Invalid path request applied: {pathRequest}");
 125            Arrive();
 126            return;
 27        }
 28
 10129        if (_context == null)
 030            BindContext(pathRequest.Context);
 10131        else if (!ReferenceEquals(_context, pathRequest.Context))
 032            throw new InvalidOperationException("NavSteering cannot accept a path request from a different TrailblazerWo
 33
 10134        _hasLineOfSightPath = false;
 10135        _isAtDestination = false;
 36
 10137        _stoppedFrameCount = 0;
 10138        _isStuck = false;
 10139        _stuckFrameCount = 0;
 40
 10141        _shouldMove = true;
 42        // NOTE: destination can be an exact point within a voxel, not neccesarily the voxel position
 10143        _requestedDestination = pathRequest.TargetPosition;
 10144        _destination = _requestedDestination;
 45
 10146        ReleaseTrailGuide();
 10147        _currentRequest = pathRequest;
 10148        _lastUnitSize = pathRequest.UnitSize;
 49
 10150        _repathTries = 0;
 10151        _shouldRequestPathThisFrame = true;
 10152        PublishRouteTopology(hasResolvedTopology: false, usesGuideTopology: false, requestsClimbIntent: false, force: tr
 53
 10154        AddToMovementGroup(groupId);
 10155        UpdateMovementGroupState(pathRequest.Origin, true);
 56
 10157        Events.OnMoveRequestApplied?.Invoke();
 158    }
 59
 60    /// <summary>
 61    /// Applies a short delay to prevent auto-stopping behavior for a few frames.
 62    /// </summary>
 63    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1264    public void PauseAutoStop() => _autoStopFrameCount = AutoPauseStopTimeForContext;
 65
 66    /// <summary>
 67    /// Replaces the current guide used for guided steering.
 68    /// </summary>
 69    /// <param name="guide">The guide to follow, or <c>null</c> to clear guided movement.</param>
 70    public void SetTrailGuide(IGuide? guide)
 71    {
 472        _trailGuide = guide;
 473        _shouldRequestPathThisFrame = _trailGuide != null;
 474    }
 75
 76    /// <summary>
 77    /// Assigns this steering session to a movement group.
 78    /// </summary>
 79    /// <param name="groupId">A non-negative group identifier. Negative values remove the current group assignment.</par
 80    public void AddToMovementGroup(int groupId)
 81    {
 10182        if (groupId < 0)
 83        {
 5684            LeaveMovementGroup();
 5685            return;
 86        }
 87
 4588        if (MovementGroupID >= 0 && MovementGroupID != groupId)
 189            LeaveMovementGroup();
 90
 4591        MovementGroupID = groupId;
 4592        _movementGroupMode = MovementGroupTravelMode.Individual;
 4593    }
 94
 95    /// <summary>
 96    /// Removes this steering session from its current movement group.
 97    /// </summary>
 98    public void LeaveMovementGroup()
 99    {
 283100        RemoveMovementGroupSession();
 283101        MovementGroupID = -1;
 283102        GroupIndex = -1;
 283103        _movementGroupMode = MovementGroupTravelMode.None;
 283104        _destination = _requestedDestination;
 283105    }
 106
 107    /// <summary>
 108    /// Rebuilds this steering session's shared movement-group membership from the current runtime owner state.
 109    /// </summary>
 110    /// <remarks>
 111    /// Call this after loading multiple grouped steering sessions when you want the coordinator warmed
 112    /// before the next simulation frame. If it is skipped, grouped steering will still recover lazily
 113    /// during <see cref="GetHeading(ISteer)"/>.
 114    /// </remarks>
 115    /// <param name="vessel">The current steering owner whose position, radius, and stable id should seed the coordinato
 116    public void PrewarmMovementGroup(ISteer vessel)
 117    {
 5118        SwiftThrowHelper.ThrowIfNull(vessel, nameof(vessel));
 119
 4120        if (!ShouldMove || !IsInGroup || _currentRequest == null)
 1121            return;
 122
 3123        MovementGroups.Prewarm(
 3124            _movementGroupSession,
 3125            vessel.GlobalId,
 3126            _requestedDestination,
 3127            vessel.Position,
 3128            _agentRadius);
 3129    }
 130
 131    #endregion
 132}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.Serialization.cs

#LineLine coverage
 1using Chronicler;
 2using FixedMathSharp;
 3using Trailblazer.Navigation.MovementGroups;
 4using Trailblazer.Pathing;
 5
 6namespace Trailblazer.Navigation.Steering;
 7
 8public partial class NavSteering
 9{
 10    #region Serialization
 11
 12    /// <inheritdoc />
 13    public virtual void RecordData(IChronicler chronicler)
 14    {
 9015        var requestRecord = new PathRequestRecord();
 9016        if (chronicler.Mode == SerializationMode.Saving)
 4517            requestRecord.Capture(_currentRequest, _trailGuide);
 18
 9019        int movementGroupId = _movementGroupSession.GroupId;
 20
 9021        RecordValues.Look(chronicler, ref CanPathfind, "CanPathfind", true);
 9022        RecordValues.Look(chronicler, ref _destination, "Destination", Vector3d.Zero);
 9023        RecordValues.Look(chronicler, ref _requestedDestination, "RequestedDestination", Vector3d.Zero);
 9024        RecordValues.Look(chronicler, ref _lastUnitSize, "LastUnitSize", Fixed64.Zero);
 9025        RecordValues.Look(chronicler, ref PathRecheckCooldownFrames, "PathRecheckCooldownFrames", DefaultPathRecheckCool
 9026        RecordValues.Look(chronicler, ref _targetDirection, "TargetDirection", Vector3d.Zero);
 9027        RecordValues.Look(chronicler, ref _lastTargetDirection, "LastTargetDirection", Vector3d.Zero);
 9028        RecordValues.Look(chronicler, ref _shouldMove, "ShouldMove", false);
 9029        RecordValues.Look(chronicler, ref _isStuck, "IsStuck", false);
 9030        RecordValues.Look(chronicler, ref _hasLineOfSightPath, "HasLineOfSightPath", false);
 9031        RecordValues.Look(chronicler, ref _currentRouteHasResolvedTopology, "CurrentRouteHasResolvedTopology", false);
 9032        RecordValues.Look(chronicler, ref _currentRouteUsesGuideTopology, "CurrentRouteUsesGuideTopology", false);
 9033        RecordValues.Look(chronicler, ref _currentRouteRequestsClimbIntent, "CurrentRouteRequestsClimbIntent", false);
 9034        RecordValues.Look(chronicler, ref _currentRouteTopologyVersion, "CurrentRouteTopologyVersion", 0);
 9035        RecordValues.Look(chronicler, ref _shouldRequestPathThisFrame, "ShouldRequestPathThisFrame", false);
 9036        RecordValues.Look(chronicler, ref _pathCheckCooldown, "PathCheckCooldown", 0);
 9037        RecordValues.Look(chronicler, ref _distanceToTarget, "DistanceToTarget", Fixed64.Zero);
 9038        RecordValues.Look(chronicler, ref _isAtDestination, "IsAtDestination", false);
 9039        RecordValues.Look(chronicler, ref CanMove, "CanMove", false);
 9040        RecordValues.Look(chronicler, ref _stoppedFrameCount, "StoppedFrameCount", 0);
 9041        RecordValues.Look(chronicler, ref _autoStopFrameCount, "AutoStopFrameCount", 0);
 9042        RecordValues.Look(chronicler, ref _repathTries, "RepathTries", 0);
 9043        RecordValues.Look(chronicler, ref _stuckFrameCount, "StuckFrameCount", 0);
 9044        RecordValues.Look(chronicler, ref StopMultiplier, "StopMultiplier", DefaultDirectStop);
 9045        RecordValues.Look(chronicler, ref GroupFactor, "GroupFactor", DefaultGroupFactor);
 9046        RecordValues.Look(chronicler, ref AvoidFactor, "AvoidFactor", DefaultAvoidFactor);
 9047        RecordValues.Look(chronicler, ref BehaviorWeights, "BehaviorWeights", DefaultBehaviorWeights);
 9048        RecordValues.Look(chronicler, ref BrakingPower, "BrakingPower", DefaultBrakingPower);
 9049        RecordValues.Look(chronicler, ref movementGroupId, "MovementGroupId", 0);
 9050        RecordValues.Look(chronicler, ref _movementGroupMode, "MovementGroupMode", MovementGroupTravelMode.None);
 9051        RecordDeep.Look(chronicler, ref requestRecord, "PathRequest");
 52
 9053        if (chronicler.Mode == SerializationMode.Loading)
 54        {
 4555            ReleaseTrailGuide();
 4556            ResetMovementGroupSession();
 57
 4558            _currentRequest = null;
 4559            if (!requestRecord.TryCreateRequest(ResolveContext(), out IPathRequest? request))
 60            {
 461                _shouldMove = false;
 462                _isStuck = false;
 463                _hasLineOfSightPath = false;
 464                _shouldRequestPathThisFrame = false;
 465                _destination = Vector3d.Zero;
 466                _requestedDestination = Vector3d.Zero;
 467                _targetDirection = Vector3d.Zero;
 468                _lastTargetDirection = Vector3d.Zero;
 469                _distanceToTarget = Fixed64.Zero;
 470                _movementGroupMode = MovementGroupTravelMode.None;
 71            }
 72            else
 73            {
 4174                _currentRequest = request;
 75            }
 76
 4577            MovementGroupID = movementGroupId;
 4578            GroupIndex = -1;
 4579            if (movementGroupId < 0)
 2180                _movementGroupMode = MovementGroupTravelMode.None;
 81
 4582            if (_currentRequest != null
 4583                && ShouldMove
 4584                && requestRecord.HasGuide
 4585                && !_shouldRequestPathThisFrame
 4586                && !HasLineOfSightPath)
 87            {
 1188                if (!requestRecord.TryCreateGuide(_currentRequest, out _trailGuide))
 189                    _shouldRequestPathThisFrame = ShouldMove;
 90            }
 3491            else if (_currentRequest != null
 3492                && ShouldMove
 3493                && !HasLineOfSightPath)
 94            {
 1895                _shouldRequestPathThisFrame = true;
 96            }
 97        }
 8998    }
 99
 100    private void ReleaseTrailGuide(bool dispose = false)
 101    {
 213102        if (_trailGuide == null)
 210103            return;
 104
 3105        (_currentRequest?.Context ?? ResolveContext()).Guides.ReturnGuide(_trailGuide, dispose);
 3106        _trailGuide = null;
 3107    }
 108
 109    private void ResetMovementGroupSession()
 110    {
 45111        RemoveMovementGroupSession();
 45112        _movementGroupSession.Reset();
 45113    }
 114
 115    #endregion
 116}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Steering/NavSteering.Simulation.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using System;
 4using System.Runtime.CompilerServices;
 5using Trailblazer.Navigation.MovementGroups;
 6using Trailblazer.Pathing;
 7
 8namespace Trailblazer.Navigation.Steering;
 9
 10public partial class NavSteering
 11{
 12    #region Simulation Lifecycle
 13
 14    /// <summary>
 15    /// Initializes the object by setting up its defaults, events, traversal state, and movement controller.
 16    /// </summary>
 17    protected virtual void OnInitialize(Fixed64 radius)
 18    {
 20219        UpdateOwnerRadius(radius);
 20
 20221        LeaveMovementGroup();
 22
 20223        _stoppedFrameCount = 0;
 20224        _autoStopFrameCount = 0;
 25
 20226        StopMultiplier = DefaultDirectStop;
 27
 20228        _shouldRequestPathThisFrame = false;
 20229        _hasLineOfSightPath = false;
 20230        _shouldMove = false;
 31
 20232        _isStuck = false;
 20233        _stuckFrameCount = 0;
 20234        _repathTries = 0;
 35
 20236        _isAtDestination = false;
 37
 20238        _currentRequest = null;
 20239        _trailGuide = null;
 20240        _requestedDestination = Vector3d.Zero;
 20241        _movementGroupSession.Reset();
 20242        _movementGroupMode = MovementGroupTravelMode.None;
 20243        _currentRouteHasResolvedTopology = false;
 20244        _currentRouteUsesGuideTopology = false;
 20245        _currentRouteRequestsClimbIntent = false;
 20246        _currentRouteTopologyVersion = 0;
 20247    }
 48
 49    internal virtual void UpdateOwnerRadius(Fixed64 radius)
 50    {
 51        // Fatter objects can afford to land imprecisely
 24452        _agentRadius = radius;
 24453        _closingDistance = FixedMath.Round(radius + ResolveVoxelSize());
 24454    }
 55
 56    internal void Reset()
 57    {
 358        ReleaseTrailGuide();
 359        OnInitialize(_agentRadius);
 360    }
 61
 62    private Fixed64 ResolveVoxelSize()
 63    {
 24964        if (_context != null)
 24965            return _context.VoxelSize;
 066        if (_currentRequest != null)
 067            return _currentRequest.Context.VoxelSize;
 68
 069        return GridWorld.DefaultVoxelSize;
 70    }
 71
 72    private TrailblazerWorldContext ResolveContext()
 73    {
 79974        TrailblazerWorldContext? context = _context ?? _currentRequest?.Context;
 79975        if (context != null)
 76        {
 79977            PathRequestContextResolver.ThrowIfUnusable(context);
 79978            return context;
 79        }
 80
 081        throw new InvalidOperationException("NavSteering requires an explicit TrailblazerWorldContext.");
 82    }
 83
 15084    private MovementGroupCoordinatorState MovementGroups => ResolveContext().Navigation.MovementGroups;
 85
 86    private void RemoveMovementGroupSession()
 87    {
 32888        ResolveContext().Navigation.MovementGroups.Remove(_movementGroupSession);
 32889    }
 90
 4791    private int StuckFrameThresholdForContext => ResolveFrameRate() / 4;
 92
 1293    private int AutoPauseStopTimeForContext => ResolveFrameRate() / 8;
 94
 95    private int ResolveFrameRate()
 96    {
 5997        if (_context != null)
 5998            return _context.FrameRate;
 099        if (_currentRequest != null)
 0100            return _currentRequest.Context.FrameRate;
 101
 0102        return TrailblazerClock.DefaultFrameRate;
 103    }
 104
 105    /// <summary>
 106    /// Called every simulation step to handle agent steering and movement logic.
 107    /// </summary>
 108    public virtual Vector3d GetHeading(ISteer vessel)
 109    {
 309110        CacheOwner(vessel);
 111
 309112        if (!CanMove)
 1113            return Vector3d.Zero;
 114
 308115        if (!ShouldMove || IsAtDestination)
 204116            return FinalizeIdleHeading(vessel.Speed);
 117
 104118        if (!TryEnsureCurrentRequest(out Vector3d heading))
 1119            return heading;
 120
 103121        bool usesVolumeGuidance = UsesVolumeGuidance();
 103122        UpdateMovementGroupState(vessel.Position);
 123
 103124        if (!TryPrepareMovementPathForHeading(vessel.Position, usesVolumeGuidance))
 3125            return Vector3d.Zero;
 126
 100127        UpdateTargetDirection(vessel);
 100128        if (ShouldArriveWithoutTrailGuide())
 129        {
 2130            Arrive();
 2131            return Vector3d.Zero;
 132        }
 133
 98134        if (!CheckStuckStatus(vessel.Position, vessel.Speed, vessel.StuckThresholdSpeed))
 135        {
 1136            TrailblazerLogger.DebugChannel.Info($"Stuck agent arriving!");
 1137            Arrive();
 1138            return Vector3d.Zero;
 139        }
 140
 97141        UpdateTrailGuideProgress(vessel.Acceleration, vessel.Speed);
 97142        return FinalizeHeadingFrame();
 143    }
 144
 145    /// <summary>
 146    /// Periodically called to initiate a pathfinding query based on the current position and destination.
 147    /// Note: This will run once on the next `Simulate` call after calling `ApplyPathRequest`
 148    /// </summary>
 149    protected virtual bool ValidateMovementPath(Vector3d origin)
 150    {
 151        // Unit-size change detection must run before the shouldRequestPath gate. Without this,
 152        // external TrySetUnitSize calls between frames are silently ignored when
 153        // _shouldRequestPathThisFrame is already false, and no repath ever triggers.
 93154        if (_currentRequest!.UnitSize != _lastUnitSize)
 155        {
 1156            _lastUnitSize = _currentRequest.UnitSize;
 1157            _shouldRequestPathThisFrame = true;
 158        }
 159
 93160        if (!_shouldRequestPathThisFrame)
 21161            return true;
 72162        _shouldRequestPathThisFrame = false;
 163
 164        // update origin
 72165        bool ok = _currentRequest.TrySetOrigin(origin);
 72166        if (!ok || !_currentRequest.HasValidEndpoints)
 167        {
 1168            PublishRouteTopology(hasResolvedTopology: false, usesGuideTopology: false, requestsClimbIntent: false);
 1169            TrailblazerLogger.Channel.Warn($"Path request is using invalid endpoints.");
 1170            return false;
 171        }
 172
 173        // shortcut if no path needed
 71174        if (_currentRequest.HasZeroDisplacement)
 175        {
 1176            PublishRouteTopology(hasResolvedTopology: true, usesGuideTopology: false, requestsClimbIntent: false);
 1177            return _repathTries == 0;
 178        }
 179
 70180        if (_currentRequest is VolumePathRequest volumeRequest)
 181        {
 12182            _hasLineOfSightPath = IsVolumeDestinationInSight(
 12183                _currentRequest.Context,
 12184                origin,
 12185                Destination,
 12186                _currentRequest.UnitSize,
 12187                _currentRequest.AllowUnwalkableEndpoints,
 12188                volumeRequest.Medium,
 12189                _currentRequest.StartNode,
 12190                _currentRequest.EndNode);
 191
 12192            _pathCheckCooldown = PathRecheckCooldownFrames;
 12193            if (_hasLineOfSightPath)
 194            {
 5195                ReleaseTrailGuide();
 5196                PublishRouteTopology(hasResolvedTopology: true, usesGuideTopology: false, requestsClimbIntent: false);
 5197                return true;
 198            }
 199        }
 200        else
 201        {
 58202            _hasLineOfSightPath = IsDestinationInSight(
 58203                _currentRequest.Context,
 58204                origin,
 58205                Destination,
 58206                _currentRequest.UnitSize,
 58207                _currentRequest.AllowUnwalkableEndpoints);
 58208            if (_hasLineOfSightPath)
 209            {
 31210                PublishRouteTopology(hasResolvedTopology: true, usesGuideTopology: false, requestsClimbIntent: false);
 31211                return true;  // no path required
 212            }
 213        }
 214
 215        // request guide
 34216        ReleaseTrailGuide();
 34217        _pathCheckCooldown = PathRecheckCooldownFrames;
 34218        if (!_currentRequest.IsValid || !_currentRequest.Context.Guides.RequestGuide(_currentRequest, out _trailGuide))
 219        {
 1220            PublishRouteTopology(hasResolvedTopology: false, usesGuideTopology: false, requestsClimbIntent: false);
 1221            TrailblazerLogger.Channel.Warn($"Unable to retrieve a guide from {origin} to {Destination}.");
 1222            return false;
 223        }
 224
 33225        PublishRouteTopology(
 33226            hasResolvedTopology: true,
 33227            usesGuideTopology: true,
 33228            requestsClimbIntent: GuidedClimbIntentResolver.Resolve(_currentRequest));
 33229        return true;
 230    }
 231
 232    /// <summary>
 233    /// Computes the steering direction toward the destination or along the path.
 234    /// </summary>
 235    protected virtual Vector3d FindTargetDirection(Vector3d position)
 236    {
 91237        Vector3d targetDirection = Vector3d.Zero;
 91238        if (HasLineOfSightPath)
 46239            targetDirection = Destination - position;
 45240        else if (HasTrailGuide)
 241        {
 44242            if (_trailGuide is IWaypointGuide waypointGuide)
 38243                targetDirection = waypointGuide.GetCurrentWaypointDirection(position);
 244            else
 6245                _trailGuide!.TryGetMovementDirection(position, out targetDirection);
 246        }
 247
 91248        if (targetDirection == Vector3d.Zero)
 249        {
 24250            TrailblazerLogger.DebugChannel.Info($"No viable movement direction found.");
 24251            return Vector3d.Zero;
 252        }
 253
 254        // This is now the direction we want to be travelling in
 67255        return targetDirection.Normalize(out _distanceToTarget);
 256    }
 257
 258    /// <summary>
 259    /// Returns true if we’re within closing distance _and_ our heading has flipped,
 260    /// or if we’re very close relative to voxel size.
 261    /// </summary>
 262    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 263    public bool ShouldAdvanceToNextWaypoint()
 264    {
 17265        return (_distanceToTarget < _closingDistance
 17266                    && Vector3d.Dot(TargetDirection, LastTargetDirection) < Fixed64.Epsilon)
 17267            || _distanceToTarget < _closingDistance * ResolveVoxelSize();
 268    }
 269
 270    /// <summary>
 271    /// Evaluates the agent's current movement direction and velocity, updating stuck and arrival state.
 272    /// </summary>
 273    protected virtual bool CheckStuckStatus(
 274        Vector3d position,
 275        Fixed64 speed,
 276        Fixed64 stuckThreshold)
 277    {
 98278        if (!CanAutoStop)
 9279            return true;
 280
 89281        if (stuckThreshold <= Fixed64.Zero || speed >= stuckThreshold)
 42282            return ResetStuckStatus();
 283
 47284        _stuckFrameCount++;
 47285        if (_stuckFrameCount <= StuckFrameThresholdForContext)
 41286            return true;
 287
 6288        return _repathTries < StuckRepathTries
 6289            ? TryRecoverFromStuck(position)
 6290            : DeclareHardStuck();
 291    }
 292
 293    private Vector3d FinalizeIdleHeading(Fixed64 speed)
 294    {
 204295        _targetDirection = Vector3d.Zero;
 204296        if (speed <= Fixed64.Epsilon)
 203297            _stoppedFrameCount++;
 298
 204299        return FinalizeHeadingFrame();
 300    }
 301
 302    private bool TryEnsureCurrentRequest(out Vector3d heading)
 303    {
 104304        if (_currentRequest != null)
 305        {
 103306            heading = Vector3d.Zero;
 103307            return true;
 308        }
 309
 1310        Arrive();
 1311        heading = TargetDirection;
 1312        return false;
 313    }
 314
 315    private bool TryPrepareMovementPathForHeading(Vector3d position, bool usesVolumeGuidance)
 316    {
 103317        if ((CanPathfind || usesVolumeGuidance) && !ValidateMovementPath(position))
 318        {
 2319            HandleInvalidPath("Invalid path detected!");
 2320            return false;
 321        }
 322
 101323        RefreshLineOfSightState(position);
 101324        if (usesVolumeGuidance && !HasLineOfSightPath && !HasTrailGuide && !ValidateMovementPath(position))
 325        {
 1326            HandleInvalidPath("Invalid volume path detected!");
 1327            return false;
 328        }
 329
 100330        return true;
 331    }
 332
 333    private void RefreshLineOfSightState(Vector3d position)
 334    {
 101335        if (_pathCheckCooldown > 0)
 71336            return;
 337
 30338        if (_currentRequest is VolumePathRequest volumeRequest)
 339        {
 3340            _hasLineOfSightPath = IsVolumeDestinationInSight(
 3341                _currentRequest.Context,
 3342                position,
 3343                Destination,
 3344                _currentRequest.UnitSize,
 3345                _currentRequest.AllowUnwalkableEndpoints,
 3346                volumeRequest.Medium,
 3347                _currentRequest.StartNode,
 3348                _currentRequest.EndNode);
 349
 3350            if (_hasLineOfSightPath)
 351            {
 1352                ReleaseTrailGuide();
 1353                PublishRouteTopology(hasResolvedTopology: true, usesGuideTopology: false, requestsClimbIntent: false);
 354            }
 355        }
 356        else
 357        {
 27358            IPathRequest currentRequest = _currentRequest!;
 27359            _hasLineOfSightPath = IsDestinationInSight(
 27360                currentRequest.Context,
 27361                position,
 27362                Destination,
 27363                currentRequest.UnitSize,
 27364                currentRequest.AllowUnwalkableEndpoints);
 365
 27366            if (_hasLineOfSightPath)
 27367                PublishRouteTopology(hasResolvedTopology: true, usesGuideTopology: false, requestsClimbIntent: false);
 368        }
 369
 30370        _pathCheckCooldown = PathRecheckCooldownFrames;
 30371    }
 372
 373    private void HandleInvalidPath(string debugMessage)
 374    {
 3375        TrailblazerLogger.DebugChannel.Info($"{debugMessage}");
 3376        Events.OnInvalidPath?.Invoke();
 3377        Arrive();
 3378    }
 379
 380    private void UpdateTargetDirection(ISteer vessel)
 381    {
 100382        _lastTargetDirection = _targetDirection;
 100383        _targetDirection = FindTargetDirection(vessel.Position);
 100384        _targetDirection += ComputeCombinedSteering(
 100385            vessel.Position,
 100386            vessel.Velocity,
 100387            vessel.Speed,
 100388            vessel.Radius,
 100389            vessel.GlobalId);
 100390    }
 391
 392    private bool ShouldArriveWithoutTrailGuide()
 393    {
 100394        if (HasTrailGuide)
 53395            return false;
 396
 47397        Fixed64 moveAmount = FixedMath.Clamp01(TargetDirection.Magnitude);
 47398        bool reachedTarget = _distanceToTarget < _closingDistance * GetActiveStopMultiplier();
 47399        bool noInput = moveAmount == Fixed64.Zero;
 47400        return reachedTarget || (!IsStuck && noInput);
 401    }
 402
 403    private void UpdateTrailGuideProgress(Vector3d acceleration, Fixed64 speed)
 404    {
 97405        if (TargetDirection == Vector3d.Zero)
 26406            return;
 407
 71408        if (_trailGuide is IWaypointGuide waypointGuide && ShouldAdvanceToNextWaypoint())
 17409            waypointGuide.AdvanceWaypoint();
 410
 71411        if (HasTrailGuide)
 31412            SetDeceleration(acceleration, speed);
 71413    }
 414
 415    private Vector3d FinalizeHeadingFrame()
 416    {
 301417        _autoStopFrameCount--;
 301418        _pathCheckCooldown--;
 419
 301420        Events.OnStartTraversal?.Invoke(TargetDirection);
 301421        return TargetDirection;
 422    }
 423
 424    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 425    private bool ResetStuckStatus()
 426    {
 42427        _isStuck = false;
 42428        _stuckFrameCount = 0;
 42429        _repathTries = 0;
 42430        return true;
 431    }
 432
 433    private bool TryRecoverFromStuck(Vector3d position)
 434    {
 5435        _hasLineOfSightPath = false;
 436
 5437        if (IsInGroup)
 0438            LeaveMovementGroup();
 439
 5440        if (TryApplyFallbackDirection(position))
 1441            return true;
 442
 4443        PreparePathRetry();
 4444        _repathTries++;
 4445        return true;
 446    }
 447
 448    private bool TryApplyFallbackDirection(Vector3d position)
 449    {
 5450        if (!HasTrailGuide || _trailGuide!.TryGetFallbackDirection(position, out Vector3d fallback) == false)
 4451            return false;
 452
 1453        _targetDirection = fallback;
 1454        _repathTries++;
 1455        _stuckFrameCount = 0;
 1456        return true;
 457    }
 458
 459    private void PreparePathRetry()
 460    {
 4461        _targetDirection = Vector3d.Zero;
 4462        _shouldRequestPathThisFrame = true;
 4463        DisposeCurrentTrailGuide();
 4464    }
 465
 466    private bool DeclareHardStuck()
 467    {
 1468        _isStuck = true;
 1469        DisposeCurrentTrailGuide();
 1470        Events.OnIsStuck?.Invoke();
 1471        return false;
 472    }
 473
 474    private void DisposeCurrentTrailGuide()
 475    {
 5476        if (_trailGuide == null)
 5477            return;
 478
 0479        (_currentRequest?.Context ?? ResolveContext()).Guides.ReturnGuide(_trailGuide, true);
 0480        _trailGuide = null;
 0481    }
 482
 483    /// <summary>
 484    /// Adjusts the target direction to decelerate the object as it approaches its destination based on the specified
 485    /// acceleration and current speed.
 486    /// </summary>
 487    /// <remarks>
 488    /// This method is intended to be overridden in derived classes to customize deceleration behavior.
 489    /// It modulates the target direction to ensure smooth slowing as the object nears its target.
 490    /// </remarks>
 491    /// <param name="acceleration">
 492    /// The acceleration vector used to determine the deceleration rate.
 493    /// If the vector is zero, a default braking power is used.
 494    /// </param>
 495    /// <param name="speed">The current speed of the object, used to calculate the distance required to slow down.</para
 496    protected virtual void SetDeceleration(Vector3d acceleration, Fixed64 speed)
 497    {
 498        // Scaling direction before passing to the motor lets us
 499        // modulate movement before acceleration is applied
 32500        Fixed64 deceleration = acceleration != Vector3d.Zero
 32501            ? acceleration.Magnitude
 32502            : BrakingPower;
 32503        Fixed64 slowDistance = speed / deceleration;
 32504        if (DistanceToTarget > Fixed64.Epsilon && DistanceToTarget <= slowDistance)
 505        {
 1506            Fixed64 closingSpeed = DistanceToTarget / slowDistance;
 1507            _targetDirection *= closingSpeed; // reduce magnitude = slow down
 508        }
 32509    }
 510
 511    /// <summary>
 512    /// Triggers the arrival event and resets internal movement tracking.
 513    /// </summary>
 514    public void Arrive()
 515    {
 24516        StopMove();
 517
 24518        ReleaseTrailGuide();
 24519        _currentRequest = null;
 24520        _requestedDestination = Vector3d.Zero;
 24521        _distanceToTarget = Fixed64.Zero;
 24522        _isAtDestination = true;
 24523        _destination = Vector3d.Zero;
 24524        _targetDirection = Vector3d.Zero;
 525
 24526        Events.OnArrive?.Invoke();
 1527    }
 528
 529    /// <summary>
 530    /// Resets the movement and pathfinding logic, halting the agent.
 531    /// </summary>
 532    public virtual void StopMove()
 533    {
 26534        if (!_shouldMove)
 2535            return;
 536
 24537        _autoStopFrameCount = 0;
 24538        _stuckFrameCount = 0;
 24539        _stoppedFrameCount = 0;
 540
 24541        _shouldMove = false;
 24542        _shouldRequestPathThisFrame = false;
 24543        _hasLineOfSightPath = false;
 24544        PublishRouteTopology(hasResolvedTopology: false, usesGuideTopology: false, requestsClimbIntent: false, force: tr
 24545        LeaveMovementGroup();
 546
 24547        Events.OnStopMove?.Invoke();
 1548    }
 549
 550    #endregion
 551}

Methods/Properties

.cctor()
.ctor()
get_Destination()
get_TargetDirection()
get_LastTargetDirection()
get_CurrentRequest()
get_TrailGuide()
get_ShouldMove()
get_IsStuck()
get_HasLineOfSightPath()
get_CurrentRouteRequestsClimbIntent()
get_CurrentRouteTopologyVersion()
get_DistanceToTarget()
get_HasTrailGuide()
get_IsAtDestination()
get_StoppedFrameCount()
get_CanAutoStop()
get_Context()
CreateNew(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Fixed64)
.ctor(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Fixed64)
BindContext(Trailblazer.TrailblazerWorldContext)
get_MovementGroupID()
set_MovementGroupID(System.Int32)
get_GroupIndex()
set_GroupIndex(System.Int32)
get_IsInGroup()
ComputeCombinedSteering(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64,System.Guid)
CacheOwner(Trailblazer.Navigation.ISteer)
UpdateMovementGroupState(FixedMathSharp.Vector3d,System.Boolean)
GetActiveStopMultiplier()
IsGroupNeighbor(System.Guid,System.Int32)
UsesVolumeGuidance()
PublishRouteTopology(System.Boolean,System.Boolean,System.Boolean,System.Boolean)
IsDestinationInSight(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,System.Boolean)
IsVolumeDestinationInSight(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,System.Boolean,Trailblazer.TraversalMedium,GridForge.Grids.Voxel,GridForge.Grids.Voxel)
ApplyPathRequest(Trailblazer.Pathing.IPathRequest,System.Int32)
PauseAutoStop()
SetTrailGuide(Trailblazer.Pathing.IGuide)
AddToMovementGroup(System.Int32)
LeaveMovementGroup()
PrewarmMovementGroup(Trailblazer.Navigation.ISteer)
RecordData(Chronicler.IChronicler)
ReleaseTrailGuide(System.Boolean)
ResetMovementGroupSession()
OnInitialize(FixedMathSharp.Fixed64)
UpdateOwnerRadius(FixedMathSharp.Fixed64)
Reset()
ResolveVoxelSize()
ResolveContext()
get_MovementGroups()
RemoveMovementGroupSession()
get_StuckFrameThresholdForContext()
get_AutoPauseStopTimeForContext()
ResolveFrameRate()
GetHeading(Trailblazer.Navigation.ISteer)
ValidateMovementPath(FixedMathSharp.Vector3d)
FindTargetDirection(FixedMathSharp.Vector3d)
ShouldAdvanceToNextWaypoint()
CheckStuckStatus(FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64)
FinalizeIdleHeading(FixedMathSharp.Fixed64)
TryEnsureCurrentRequest(FixedMathSharp.Vector3d&)
TryPrepareMovementPathForHeading(FixedMathSharp.Vector3d,System.Boolean)
RefreshLineOfSightState(FixedMathSharp.Vector3d)
HandleInvalidPath(System.String)
UpdateTargetDirection(Trailblazer.Navigation.ISteer)
ShouldArriveWithoutTrailGuide()
UpdateTrailGuideProgress(FixedMathSharp.Vector3d,FixedMathSharp.Fixed64)
FinalizeHeadingFrame()
ResetStuckStatus()
TryRecoverFromStuck(FixedMathSharp.Vector3d)
TryApplyFallbackDirection(FixedMathSharp.Vector3d)
PreparePathRetry()
DeclareHardStuck()
DisposeCurrentTrailGuide()
SetDeceleration(FixedMathSharp.Vector3d,FixedMathSharp.Fixed64)
Arrive()
StopMove()