< Summary

Line coverage
100%
Covered lines: 541
Uncovered lines: 0
Coverable lines: 541
Total lines: 1411
Line coverage: 100%
Branch coverage
100%
Covered branches: 258
Total branches: 258
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .ctor()100%11100%
File 1: get_Position()100%11100%
File 1: get_LastPosition()100%11100%
File 1: get_Rotation()100%11100%
File 1: get_Velocity()100%11100%
File 1: get_Speed()100%11100%
File 1: get_Acceleration()100%11100%
File 1: get_StuckThresholdSpeed()100%11100%
File 1: get_IsActive()100%22100%
File 1: get_Context()100%11100%
File 1: get_Steering()100%11100%
File 1: get_Turning()100%11100%
File 1: get_Motor()100%11100%
File 1: get_IsGuided()100%11100%
File 1: get_NavigationProfile()100%11100%
File 1: get_BodyShape()100%11100%
File 1: get_Radius()100%11100%
File 1: get_FootPosition()100%11100%
File 1: get_HeightmapGrounding()100%11100%
File 1: get_IsLockedOn()100%11100%
File 1: get_PendingTransition()100%11100%
File 1: get_LastCommittedCell()100%11100%
File 1: get_GlobalId()100%11100%
File 1: get_OccupantGroupId()100%11100%
File 1: .ctor(...)100%11100%
File 1: BindContext(...)100%1010100%
File 1: Activate(...)100%11100%
File 1: Activate(...)100%11100%
File 1: Setup(...)100%11100%
File 1: Setup(...)100%1212100%
File 1: Initialize(...)100%11100%
File 1: CreateLocomotionProfile()100%11100%
File 1: Reset()100%1010100%
File 1: PrewarmMovementGroup()100%11100%
File 1: ApplyInputTrekRequest(...)100%44100%
File 1: ApplyGuidedTrekRequest(...)100%22100%
File 1: ConfigureHeightmapGrounding(...)100%11100%
File 1: SetFrameJumpAffordability(...)100%11100%
File 1: ToggleGuidedJump(...)100%11100%
File 1: ToggleGuidedFlight(...)100%11100%
File 1: ToggleGuidedSwim(...)100%11100%
File 1: ToggleGuidedClimb(...)100%11100%
File 1: CompletePendingTransition(...)100%66100%
File 1: NotifySteeringSessionEnded()100%22100%
File 1: SetGuidedTrekRate(...)100%11100%
File 1: TryGetTurnDirection(...)100%1010100%
File 1: Simulate()100%1616100%
File 1: CommitFrameMotion()100%1212100%
File 1: NotifyCollision()100%22100%
File 1: SetGroundContact(...)100%22100%
File 1: SetAirborne(...)100%11100%
File 1: SetWaterContact(...)100%11100%
File 1: SetTrekCondition(...)100%1212100%
File 1: SyncCurrentTrekConditionToMotor()100%11100%
File 1: ReplaceTrekCondition(...)100%44100%
File 1: AddPositionDelta(...)100%22100%
File 1: ApplyRotationDelta(...)100%22100%
File 1: AddVelocityDelta(...)100%22100%
File 1: GenerateGUID()100%11100%
File 1: EnsureContextForSetup()100%22100%
File 1: RequireContext()100%11100%
File 1: RebuildCommittedCellState(...)100%1212100%
File 1: TryResolveCommittedCellState(...)100%66100%
File 1: RepresentsSameCellEntry(...)100%88100%
File 1: ValidateGuidedSurfaceQuery(...)100%44100%
File 1: ApplyTransitionLocomotionHints(...)100%22100%
File 1: CheckVoxelOccupancy(...)100%22100%
File 2: TryApplyHeightmapGrounding(...)100%1212100%
File 2: GetHeightmapGroundingQueryPosition()100%11100%
File 2: TryProjectRootToHeightmapSample(...)100%66100%
File 2: ProjectRootWithoutVelocity(...)100%11100%
File 2: UpdateVoxelOccupancyAfterRootProjection(...)100%11100%
File 3: RecordData(...)100%6868100%
File 3: ValidateNestedRecords(...)100%2424100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Navigator/Navigator.cs

#LineLine coverage
 1//=======================================================================
 2// Navigator.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.Runtime.CompilerServices;
 10using Chronicler;
 11using FixedMathSharp;
 12using GridForge.Grids;
 13using Trailblazer.Navigation.Motor;
 14using Trailblazer.Navigation.Steering;
 15using Trailblazer.Navigation.Turning;
 16using Trailblazer.Pathing;
 17
 18namespace Trailblazer.Navigation;
 19
 20/// <summary>
 21/// Base class for an object that owns movement, traversal state, and simulation flow.
 22/// </summary>
 23/// <remarks>
 24/// This class acts as a bridge between the simulation logic and the entity's external representation.
 25/// It defines common traversal behaviors and lifecycle methods that can be extended by concrete implementations.
 26/// </remarks>
 27[Serializable]
 28public abstract partial class Navigator : INavigate, IRecordable
 29{
 30    #region Fields
 31
 32    /// <inheritdoc cref="Position"/>
 33    protected Vector3d _position;
 34
 35    /// <inheritdoc cref="LastPosition"/>
 36    protected Vector3d _lastPosition;
 37
 38    /// <inheritdoc cref="Rotation"/>
 27739    protected FixedQuaternion _rotation = FixedQuaternion.Identity;
 40
 41    /// <inheritdoc cref="Velocity"/>
 42    protected Vector3d _velocity;
 43
 44    /// <inheritdoc cref="Speed"/>
 45    protected Fixed64 _speed;
 46
 47    /// <inheritdoc cref="Acceleration"/>
 48    protected Vector3d _acceleration;
 49
 50    /// <inheritdoc cref="NavigationProfile"/>
 51    private NavigationAgentProfile _navigationProfile;
 52
 53    /// <summary>
 54    /// Stable runtime identity used for occupancy and steering coordination.
 55    /// </summary>
 56    /// <remarks>
 57    /// By default this is allocated deterministically from object setup order.
 58    /// Hosts can override it during setup
 59    /// when a broader simulation stack already owns stable agent ids.
 60    /// </remarks>
 61    protected Guid _globalId;
 62
 27763    private byte _occupantGroupId = 1;
 64
 65    private bool _isLockedOn;
 66
 67    /// <summary>
 68    /// The controller responsible for managing the object's desired movement direction.
 69    /// </summary>
 70    protected NavSteering? _steering;
 71
 72    /// <summary>
 73    /// The controller responsible for managing the object's rotation towards the movement direction.
 74    /// </summary>
 75    protected NavTurning? _turning;
 76
 77    /// <summary>
 78    /// The controller responsible for managing the object's movement and physics interactions.
 79    /// </summary>
 80    protected NavMotor? _motor;
 81
 82    private Fixed64 _stuckThresholdSpeed;
 83
 84    private bool _isGuided;
 85
 86    /// <summary>
 87    /// The change in position to apply during the current simulation frame.
 88    /// </summary>
 89    protected Vector3d _positionDelta;
 90
 91    /// <summary>
 92    /// The change in rotation to apply during the current simulation frame.
 93    /// </summary>
 27794    protected FixedQuaternion _rotationDelta = FixedQuaternion.Identity;
 95
 96    /// <summary>
 97    /// The velocity change computed during the simulation frame.
 98    /// </summary>
 99    protected Vector3d _velocityDelta;
 100
 101    /// <summary>
 102    /// Indicates whether the Navigator has been set.
 103    /// </summary>
 104    protected bool _isSet;
 105
 106    /// <summary>
 107    /// Indicates whether the Navigator has been initialized.
 108    /// </summary>
 109    protected bool _isInitialized;
 110
 111    /// <inheritdoc cref="TrekCondition"/>
 277112    protected TrekCondition _frameCondition = new();
 113
 114    /// <inheritdoc cref="TrekRequest"/>
 277115    protected TrekRequest _frameRequest = new();
 116
 117    private NavigationTransitionInstruction? _pendingTransition;
 118
 119    private NavigationCommittedCellState? _lastCommittedCell;
 120
 121    private TrailblazerWorldContext? _context;
 122
 277123    private NavigatorHeightmapGroundingSettings _heightmapGrounding = new();
 124
 125    #endregion
 126
 127    #region State - Identity / Transform
 128
 129    /// <inheritdoc/>
 1428130    public Vector3d Position => _position;
 131
 132    /// <inheritdoc/>
 108133    public Vector3d LastPosition => _lastPosition;
 134
 135    /// <inheritdoc/>
 118136    public FixedQuaternion Rotation => _rotation;
 137
 138    /// <inheritdoc/>
 139    public Vector3d Forward { get; protected set; }
 140
 141    /// <inheritdoc/>
 315142    public Vector3d Velocity => _velocity;
 143
 144    /// <inheritdoc/>
 243145    public Fixed64 Speed => _speed;
 146
 147    /// <inheritdoc/>
 89148    public Vector3d Acceleration => _acceleration;
 149
 150    /// <summary>
 151    /// Minimum velocity threshold used to determine if the object is considered stuck.
 152    /// </summary>
 88153    public Fixed64 StuckThresholdSpeed => _stuckThresholdSpeed;
 154
 155    /// <summary>
 156    /// Indicates whether the Navigator is currently active and ready for simulation.
 157    /// </summary>
 275158    public bool IsActive => _isSet && _isInitialized;
 159
 160    /// <summary>
 161    /// Gets the world context this navigator is bound to.
 162    /// </summary>
 8163    public TrailblazerWorldContext? Context => _context;
 164
 165    #endregion
 166
 167    #region State - Controllers
 168
 169    /// <summary>
 170    /// The controller responsible for managing the object's desired movement direction.
 171    /// </summary>
 315172    public NavSteering? Steering => _steering;
 173
 174    /// <summary>
 175    /// The controller responsible for managing the object's rotation towards the movement direction.
 176    /// </summary>
 67177    public NavTurning? Turning => _turning;
 178
 179    /// <summary>
 180    /// The controller responsible for managing the object's movement and physics interactions.
 181    /// </summary>
 146182    public NavMotor? Motor => _motor;
 183
 184    /// <summary>
 185    /// Indicates whether the current traversal session owns navigation guidance.
 186    /// </summary>
 130187    public bool IsGuided => _isGuided;
 188
 189    #endregion
 190
 191    #region Settings
 192
 193    /// <summary>
 194    /// Gets the exact immutable navigation profile supplied during setup.
 195    /// </summary>
 1040196    public NavigationAgentProfile NavigationProfile => _navigationProfile;
 197
 198    /// <inheritdoc/>
 830199    public KinematicBodyShape BodyShape => NavigationProfile.Shape;
 200
 201    /// <inheritdoc/>
 489202    public Fixed64 Radius => BodyShape.Radius;
 203
 204    /// <summary>
 205    /// Gets the derived world-space foot point used by navigation and locomotion.
 206    /// </summary>
 207207    public Vector3d FootPosition => Position + Vector3d.Down * BodyShape.RootToFootOffsetY;
 208
 209    /// <summary>
 210    /// Gets the opt-in heightmap grounding settings owned by this navigator.
 211    /// </summary>
 40212    public NavigatorHeightmapGroundingSettings HeightmapGrounding => _heightmapGrounding;
 213
 214    /// <summary>
 215    /// Gets or sets a value indicating whether the object is currently locked on to a target.
 216    /// </summary>
 14217    public bool IsLockedOn { get => _isLockedOn; set => _isLockedOn = value; }
 218
 219    /// <summary>Gets the exact transition action currently awaiting host completion.</summary>
 45220    public NavigationTransitionInstruction? PendingTransition => _pendingTransition;
 221
 222    /// <summary>Gets the effective navigation cell resolved after the latest motion commit.</summary>
 49223    public NavigationCommittedCellState? LastCommittedCell => _lastCommittedCell;
 224
 225    /// <summary>Occurs after committed motion enters a different effective cell or leaves navigation data.</summary>
 226    public event Action<NavigationCommittedCellState?>? CommittedCellChanged;
 227
 228    #endregion
 229
 230    #region Voxel Occupancy
 231
 232    /// <inheritdoc cref="_globalId"/>
 1300233    public Guid GlobalId => _globalId;
 234
 235    /// <inheritdoc />
 6236    public byte OccupantGroupId { get => _occupantGroupId; set => _occupantGroupId = value; }
 237
 238    #endregion
 239
 240    #region Setup / Initialization
 241
 242    /// <summary>
 243    /// Initializes a new unbound navigator. Bind it to a context before setup.
 244    /// </summary>
 10245    protected Navigator() { }
 246
 247    /// <summary>
 248    /// Initializes a new navigator bound to the supplied world context.
 249    /// </summary>
 272250    protected Navigator(TrailblazerWorldContext context)
 251    {
 272252        BindContext(context);
 272253    }
 254
 255    /// <summary>
 256    /// Binds this navigator to a world context before setup.
 257    /// </summary>
 258    public virtual void BindContext(TrailblazerWorldContext context)
 259    {
 280260        TrailblazerWorldContext.ThrowIfUnusable(context);
 261
 280262        if (ReferenceEquals(_context, context))
 1263            return;
 264
 279265        SwiftThrowHelper.ThrowIfTrue(
 279266            _isSet || _isInitialized,
 279267            message: "Navigator context cannot be changed after setup. Call Reset() before rebinding.");
 268
 278269        _context = context;
 278270        _steering?.BindContext(context);
 278271        _motor?.BindContext(context);
 278272        _turning?.BindContext(context);
 3273    }
 274
 275    /// <summary>
 276    /// Initializes and activates the object with the specified condition, position, and optional parameters.
 277    /// </summary>
 278    /// <param name="context">The world context that owns this navigator.</param>
 279    /// <param name="condition">The condition that determines how the object is initialized and activated.</param>
 280    /// <param name="position">The position in world coordinates where the object will be placed.</param>
 281    /// <param name="rotation">The optional rotation to apply to the object. If null, a default rotation is used.</param
 282    /// <param name="velocity">The optional initial velocity of the object. If null, the object is initialized with zero
 283    /// <param name="navigationProfile">The exact body geometry and traversal capabilities owned by the object.</param>
 284    /// <param name="globalId">The optional global identifier for the object. If null, a new identifier may be generated
 285    public virtual void Activate(
 286        TrailblazerWorldContext context,
 287        TrekCondition condition,
 288        Vector3d position,
 289        NavigationAgentProfile navigationProfile,
 290        FixedQuaternion? rotation = null,
 291        Vector3d? velocity = null,
 292        Guid? globalId = null)
 293    {
 2294        BindContext(context);
 2295        Activate(condition, position, navigationProfile, rotation, velocity, globalId);
 2296    }
 297
 298    /// <summary>
 299    /// Initializes and activates the object with the specified condition, position, and optional parameters.
 300    /// </summary>
 301    /// <param name="condition">The condition that determines how the object is initialized and activated.</param>
 302    /// <param name="position">The position in world coordinates where the object will be placed.</param>
 303    /// <param name="rotation">The optional rotation to apply to the object. If null, a default rotation is used.</param
 304    /// <param name="velocity">The optional initial velocity of the object. If null, the object is initialized with zero
 305    /// <param name="navigationProfile">The exact body geometry and traversal capabilities owned by the object.</param>
 306    /// <param name="globalId">The optional global identifier for the object. If null, a new identifier may be generated
 307    public virtual void Activate(
 308        TrekCondition condition,
 309        Vector3d position,
 310        NavigationAgentProfile navigationProfile,
 311        FixedQuaternion? rotation = null,
 312        Vector3d? velocity = null,
 313        Guid? globalId = null)
 314    {
 23315        Setup(position, navigationProfile, rotation, velocity, globalId);
 21316        Initialize(condition);
 21317    }
 318
 319    /// <summary>
 320    /// Sets the initial position, exact navigation profile, rotation, velocity, and optional stable identity.
 321    /// </summary>
 322    /// <param name="context">The world context that owns this navigator.</param>
 323    /// <param name="position">Initial world-space position.</param>
 324    /// <param name="rotation">Optional starting rotation.</param>
 325    /// <param name="velocity">Optional initial velocity.</param>
 326    /// <param name="navigationProfile">The exact body geometry and traversal capabilities owned by the object.</param>
 327    /// <param name="globalId">Optional host-provided stable identity. When omitted, Trailblazer assigns one determinist
 328    public virtual void Setup(
 329        TrailblazerWorldContext context,
 330        Vector3d position,
 331        NavigationAgentProfile navigationProfile,
 332        FixedQuaternion? rotation = null,
 333        Vector3d? velocity = null,
 334        Guid? globalId = null)
 335    {
 4336        BindContext(context);
 4337        Setup(position, navigationProfile, rotation, velocity, globalId);
 4338    }
 339
 340    /// <summary>
 341    /// Sets the initial position, exact navigation profile, rotation, velocity, and optional stable identity.
 342    /// </summary>
 343    /// <param name="position">Initial world-space position.</param>
 344    /// <param name="rotation">Optional starting rotation.</param>
 345    /// <param name="velocity">Optional initial velocity.</param>
 346    /// <param name="navigationProfile">The exact body geometry and traversal capabilities owned by the object.</param>
 347    /// <param name="globalId">Optional host-provided stable identity. When omitted, Trailblazer assigns one determinist
 348    public virtual void Setup(
 349        Vector3d position,
 350        NavigationAgentProfile navigationProfile,
 351        FixedQuaternion? rotation = null,
 352        Vector3d? velocity = null,
 353        Guid? globalId = null)
 354    {
 277355        SwiftThrowHelper.ThrowIfTrue(
 277356            _isSet || _isInitialized,
 277357            message: "Navigator is already set up. Call Reset() before setting it up again.");
 358
 273359        EnsureContextForSetup();
 272360        navigationProfile.Validate(nameof(navigationProfile));
 361
 272362        SwiftThrowHelper.ThrowIfArgument(
 272363            globalId.HasValue && globalId.Value == Guid.Empty,
 272364            paramName: nameof(globalId),
 272365            message: "Navigator globalId cannot be Guid.Empty.");
 366
 271367        _globalId = globalId ?? GenerateGUID();
 368
 271369        _lastPosition = _position = position;
 271370        _rotation = rotation ?? FixedQuaternion.Identity;
 271371        if (_rotation != FixedQuaternion.Identity)
 117372            Forward = _rotation.Rotate(Vector3d.Forward);
 373        else
 154374            Forward = Vector3d.Forward;
 271375        _velocity = velocity ?? Vector3d.Zero;
 271376        _navigationProfile = navigationProfile;
 377
 271378        _isSet = true;
 271379    }
 380
 381    /// <summary>
 382    /// Initializes the object by setting up its defaults, events, traversal state, and movement controller.
 383    /// </summary>
 384    public virtual void Initialize(TrekCondition condition)
 385    {
 252386        TrailblazerWorldContext context = RequireContext();
 387
 251388        _frameCondition = condition.Clone();
 389
 251390        _steering = new NavSteering(context);
 251391        _steering.BindPendingTransitionOwner(this);
 392
 251393        _motor = new NavMotor(context, _frameCondition, CreateLocomotionProfile());
 251394        _motor.SetVelocity(Velocity);
 395
 251396        _turning = new NavTurning(context, Radius);
 397
 251398        CheckVoxelOccupancy(true);
 399
 251400        _isInitialized = true;
 251401    }
 402
 403    /// <summary>
 404    /// Creates the locomotion profile used when this object initializes its motor.
 405    /// </summary>
 406    /// <remarks>
 407    /// Override this to install a smaller or custom locomotion set per object type while preserving
 408    /// the default profile for callers that do not opt in.
 409    /// </remarks>
 410    protected virtual LocomotionProfile CreateLocomotionProfile()
 411    {
 290412        return LocomotionProfile.CreateDefault();
 413    }
 414
 415    /// <summary>
 416    /// Resets the state of the object to its initial configuration, clearing any active conditions, requests, and inter
 417    /// </summary>
 418    /// <remarks>
 419    /// Call this method to reinitialize the object for reuse or to clear any ongoing operations.
 420    /// After calling this method, the object will be in the same state as after construction, and any previous state or
 421    /// intent will be lost.
 422    /// This method is intended to be overridden in derived classes to extend the reset behavior as needed.
 423    /// </remarks>
 424    public virtual void Reset()
 425    {
 29426        _frameCondition.Reset();
 29427        _frameRequest.Reset();
 29428        _isGuided = false;
 29429        _pendingTransition = null;
 29430        _lastCommittedCell = null;
 29431        _heightmapGrounding.Reset();
 29432        _steering?.UnbindPendingTransitionOwner(this);
 29433        _steering?.Reset();
 434
 29435        if (_context != null && !_context.IsDisposed && _context.World.IsActive)
 26436            GridOccupantManager.TryDeregister(_context.World, this);
 437
 29438        _isSet = false;
 29439        _isInitialized = false;
 29440        _context = null;
 29441    }
 442
 443    #endregion
 444
 445    #region Host Bindings
 446
 447    /// <summary>
 448    /// Prewarms the steering movement-group coordinator from this object's currently loaded state.
 449    /// </summary>
 450    /// <remarks>
 451    /// This is primarily useful after loading several grouped navigators through Chronicler. Call it once
 452    /// for each loaded object before the next simulation frame if you want movement-group formation state
 453    /// available immediately. If it is skipped, grouped steering will still rejoin lazily on the next update.
 454    /// </remarks>
 455    public virtual void PrewarmMovementGroup()
 456    {
 3457        SwiftThrowHelper.ThrowIfTrue(
 3458            !IsActive,
 3459            message: "Navigator must be Setup and Initialized before prewarming movement groups.");
 460
 1461        Steering!.PrewarmMovementGroup(this);
 1462    }
 463
 464    #endregion
 465
 466    #region Input / Travel Requests
 467
 468    /// <summary>
 469    /// Constructs and applies a traversal request using high-level navigation input values.
 470    /// </summary>
 471    /// <param name="direction">Desired direction of travel.</param>
 472    /// <param name="rate">Rate of travel (walk, run, etc.).</param>
 473    /// <param name="isRequestingJump">Whether the agent is requesting a jump action.</param>
 474    /// <param name="isRequestingFlight">Whether the agent is requesting to fly or glide.</param>
 475    /// <param name="isRequestingSwim">Whether the agent is requesting active swim control while in liquid.</param>
 476    /// <param name="isRequestingClimb">Whether the agent is requesting to climb or maintain climb intent.</param>
 477    /// <param name="facingDirection">Optional world-space facing direction to use instead of facing along the movement 
 478    /// <param name="canAffordJump">Frame-owned jump affordability answer for this request.</param>
 479    public virtual void ApplyInputTrekRequest(
 480        Vector3d? direction = null,
 481        TrekRate? rate = null,
 482        Vector3d? facingDirection = null,
 483        bool? isRequestingFlight = null,
 484        bool? isRequestingSwim = null,
 485        bool? isRequestingClimb = null,
 486        bool? isRequestingJump = null,
 487        bool canAffordJump = true)
 488    {
 18489        if (!IsActive) return;
 490
 16491        Steering!.StopMove();
 16492        _isGuided = false;
 16493        _pendingTransition = null;
 16494        _frameRequest.SetRequest(
 16495                direction: direction ?? Vector3d.Zero,
 16496                rate: rate ?? TrekRate.Stationary,
 16497                isRequestingJump: isRequestingJump ?? false,
 16498                isRequestingFlight: isRequestingFlight ?? false,
 16499                isRequestingSwim: isRequestingSwim ?? false,
 16500                isRequestingClimb: isRequestingClimb ?? false,
 16501                facingDirection: facingDirection,
 16502                canAffordJump: canAffordJump
 16503        );
 16504    }
 505
 506    /// <summary>
 507    /// Applies complete immutable graph-backed A* or flow-field intent.
 508    /// </summary>
 509    /// <param name="query">The exact query intent whose start point equals the current derived foot position.</param>
 510    /// <param name="rate">Desired movement rate.</param>
 511    /// <param name="isRequestingFlight">Whether the object intends to fly or glide during traversal.</param>
 512    /// <param name="isRequestingSwim">Whether the object intends to actively swim during traversal.</param>
 513    /// <param name="isRequestingClimb">Whether the object explicitly intends to climb during traversal.</param>
 514    /// <param name="isRequestingJump">Whether the object intends to jump during traversal.</param>
 515    /// <param name="canAffordJump">Frame-owned jump affordability answer for this request.</param>
 516    /// <param name="groupId">Optional shared movement-group identifier.</param>
 517    /// <exception cref="ArgumentException">Thrown when the query does not match the Navigator's exact navigation profil
 518    public virtual void ApplyGuidedTrekRequest(
 519        PathQuery query,
 520        TrekRate? rate = null,
 521        bool? isRequestingFlight = null,
 522        bool? isRequestingSwim = null,
 523        bool? isRequestingClimb = null,
 524        bool? isRequestingJump = null,
 525        bool canAffordJump = true,
 526        int groupId = -1)
 527    {
 105528        if (!IsActive)
 1529            return;
 530
 104531        ValidateGuidedSurfaceQuery(query);
 101532        _pendingTransition = null;
 101533        _isGuided = true;
 101534        _frameRequest.SetRequest(
 101535            direction: Vector3d.Zero,
 101536            rate: rate ?? TrekRate.Stationary,
 101537            isRequestingJump: isRequestingJump ?? false,
 101538            isRequestingFlight: isRequestingFlight ?? false,
 101539            isRequestingSwim: isRequestingSwim ?? false,
 101540            isRequestingClimb: isRequestingClimb ?? false,
 101541            facingDirection: null,
 101542            canAffordJump: canAffordJump);
 543
 101544        Steering!.ApplyPathQuery(query, groupId);
 101545    }
 546
 547    /// <summary>
 548    /// Configures explicit heightmap grounding for concrete navigators that opt in during traversal checks.
 549    /// </summary>
 550    /// <param name="mode">Heightmap grounding behavior to use when <see cref="TryApplyHeightmapGrounding"/> is called.<
 551    /// <param name="layerName">Optional initial layer preference used before an active layer is established.</param>
 552    /// <param name="groundOffset">Optional extra root offset above the sampled ground Y.</param>
 553    /// <param name="snapTolerance">Optional maximum root-Y correction allowed for positional projection.</param>
 554    public virtual void ConfigureHeightmapGrounding(
 555        HeightmapGroundingMode mode,
 556        string? layerName = null,
 557        Fixed64? groundOffset = null,
 558        Fixed64? snapTolerance = null)
 559    {
 27560        _heightmapGrounding.Configure(mode, layerName, groundOffset, snapTolerance);
 25561    }
 562
 563    /// <summary>
 564    /// Sets the frame-owned jump affordability snapshot used by <see cref="NavMotor"/> on the next traversal step.
 565    /// </summary>
 566    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1567    public virtual void SetFrameJumpAffordability(bool canAffordJump) => _frameRequest.CanAffordJump = canAffordJump;
 568
 569    /// <summary>
 570    /// Called to make the agent jump if allowed and in a valid state.
 571    /// </summary>
 572    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1573    public virtual void ToggleGuidedJump(bool status) => _frameRequest.IsRequestingJump = status;
 574
 575    /// <summary>
 576    /// Called to toggle controlled flight if supported by the installed locomotion profile.
 577    /// </summary>
 578    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1579    public virtual void ToggleGuidedFlight(bool status) => _frameRequest.IsRequestingFlight = status;
 580
 581    /// <summary>
 582    /// Called to toggle active swim control if supported by the installed locomotion profile.
 583    /// </summary>
 584    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1585    public virtual void ToggleGuidedSwim(bool status) => _frameRequest.IsRequestingSwim = status;
 586
 587    /// <summary>
 588    /// Called to toggle climb intent if supported by the installed locomotion profile.
 589    /// </summary>
 590    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 591    public virtual void ToggleGuidedClimb(bool status)
 1592        => _frameRequest.IsRequestingClimb = status;
 593
 594    /// <summary>Completes the exact pending semantic action owned by the active guide.</summary>
 595    public virtual NavigationGuideStatus CompletePendingTransition(
 596        in NavigationTransitionInstruction instruction)
 597    {
 12598        if (_pendingTransition == null || Steering == null)
 3599            return NavigationGuideStatus.Stale;
 600
 9601        NavigationGuideStatus status = Steering.CompletePendingTransition(instruction);
 9602        if (status != NavigationGuideStatus.Success)
 4603            return status;
 604
 5605        TraversalTransitionLocomotionHints hints = _pendingTransition.Value.LocomotionHints;
 5606        _pendingTransition = null;
 5607        ApplyTransitionLocomotionHints(hints, pending: false);
 5608        return NavigationGuideStatus.Success;
 609    }
 610
 611    internal void NotifySteeringSessionEnded()
 612    {
 40613        _isGuided = false;
 40614        if (_pendingTransition == null)
 34615            return;
 616
 6617        _pendingTransition = null;
 6618        ApplyTransitionLocomotionHints(TraversalTransitionLocomotionHints.None, pending: false);
 6619    }
 620
 621    /// <summary>
 622    /// Changes the speed at which the object is currently traveling without altering direction.
 623    /// </summary>
 624    /// <param name="rate">New traversal rate to apply (walk, run, etc.).</param>
 625    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1626    public virtual void SetGuidedTrekRate(TrekRate rate) => _frameRequest.Rate = rate;
 627
 628    #endregion
 629
 630    #region Simulation Lifecycle
 631
 632    /// <summary>
 633    /// Attempts to resolve the direction the object should try to face for the current frame.
 634    /// </summary>
 635    protected virtual bool TryGetTurnDirection(TrekRequest request, out Vector3d turnDirection)
 636    {
 30637        if (request.FacingDirection.HasValue && request.FacingDirection.Value != Vector3d.Zero)
 638        {
 2639            turnDirection = request.FacingDirection.Value;
 2640            return true;
 641        }
 642
 643        // Lock-on strafing/backpedaling keeps the current facing unless the host
 644        // explicitly supplies a facing override or the request is treated as sprinting.
 28645        if (!IsGuided
 28646            && IsLockedOn
 28647            && request.Rate != TrekRate.Fast)
 648        {
 1649            turnDirection = Vector3d.Zero;
 1650            return false;
 651        }
 652
 27653        turnDirection = request.Direction;
 27654        return turnDirection != Vector3d.Zero;
 655    }
 656
 657    /// <summary>
 658    /// Runs simulation logic for this object (input handling, steering, etc.).
 659    /// </summary>
 660    public virtual void Simulate()
 661    {
 31662        SwiftThrowHelper.ThrowIfTrue(
 31663            !IsActive,
 31664            message: "Navigator must be Setup and Initialized before Simulate().");
 665
 30666        Vector3d heading = Vector3d.Zero;
 30667        if (IsGuided)
 668        {
 24669            if (_pendingTransition is NavigationTransitionInstruction pending)
 670            {
 4671                if (Steering!.PendingTransitionGuideStatus == NavigationGuideStatus.Stale)
 2672                    Steering.StopMove();
 673                else
 2674                    ApplyTransitionLocomotionHints(pending.LocomotionHints, pending: true);
 675            }
 676            else
 677            {
 20678                heading = Steering!.GetHeading(this, out NavigationTransitionInstruction? transition);
 20679                if (transition.HasValue)
 680                {
 17681                    _pendingTransition = transition;
 17682                    ApplyTransitionLocomotionHints(transition.Value.LocomotionHints, pending: true);
 17683                    heading = Vector3d.Zero;
 684                }
 685            }
 686        }
 687
 30688        _frameRequest.SetTransientState(
 30689             origin: Position,
 30690             footPosition: FootPosition,
 30691             rotation: Rotation,
 30692             direction: IsGuided ? heading : null
 30693        );
 694
 30695        if (TryGetTurnDirection(_frameRequest, out Vector3d turnDirection))
 6696            Turning!.RequestTurnDirection(Forward, turnDirection);
 697
 30698        if (Motor!.TryTraversal(_frameRequest, out Vector3d vDelta, out Vector3d pDelta, out FixedQuaternion rDelta))
 699        {
 26700            AddVelocityDelta(vDelta);
 26701            AddPositionDelta(pDelta);
 26702            ApplyRotationDelta(rDelta);
 703        }
 704
 30705        if (Turning!.TrySimulateTurn(_position, _lastPosition, Forward, _rotation, out FixedQuaternion appliedRotation))
 4706            _rotation = appliedRotation;
 30707    }
 708
 709    /// <summary>
 710    /// Finalizes traversal by updating movement calculations and applying corrections.
 711    /// </summary>
 712    /// <remarks>
 713    /// Should be called once every rendering/player interfacing frame,
 714    /// after physics bodies apply velocity changes.
 715    /// </remarks>
 716    public virtual void CommitFrameMotion()
 717    {
 34718        SwiftThrowHelper.ThrowIfTrue(
 34719            !IsActive,
 34720            message: "Navigator must be Setup and Initialized before CommitFrameMotion().");
 721
 33722        _lastPosition = _position;
 33723        _position += _positionDelta + _velocityDelta;
 724
 33725        CheckVoxelOccupancy();
 726
 33727        if (_rotationDelta != FixedQuaternion.Identity)
 728        {
 1729            _rotation *= _rotationDelta;
 1730            _rotationDelta = FixedQuaternion.Identity;
 731        }
 732
 33733        if (_rotation != FixedQuaternion.Identity)
 6734            Forward = _rotation.Rotate(Vector3d.Forward);
 735        else
 27736            Forward = Vector3d.Forward;
 737
 33738        CheckTrekCondition();
 739
 33740        Vector3d previousVelocity = _velocity;
 33741        TrailblazerWorldContext context = RequireContext();
 33742        Fixed64 invDelta = context.InvDeltaTime;
 33743        _velocity = (Position - LastPosition) * invDelta;
 33744        _speed = _velocity != Vector3d.Zero ? _velocity.Magnitude : Fixed64.Zero;
 33745        _acceleration = (_velocity - previousVelocity) * invDelta;
 746
 33747        if (Steering!.ShouldMove && _acceleration != Vector3d.Zero)
 6748            _stuckThresholdSpeed = (_acceleration / context.FrameRate).Magnitude;
 749        else
 27750            _stuckThresholdSpeed = Fixed64.Zero;
 751
 33752        _positionDelta = Vector3d.Zero;
 33753        _velocityDelta = Vector3d.Zero;
 754
 33755        Motor!.FinalizeTraversal(Position, LastPosition, Rotation, _frameCondition, newFootPosition: FootPosition);
 756
 757        // If the object is currently following a guided path,
 758        // reset only the transient request state to preserve path-following values.
 33759        if (IsGuided)
 11760            _frameRequest.ResetTransient();
 761        else
 22762            _frameRequest.Reset();
 763
 33764        RebuildCommittedCellState(emitChange: true);
 33765    }
 766
 767    /// <summary>
 768    /// Notifies the object that a collision occurred so collision-driven subsystem responses can run on the next simula
 769    /// </summary>
 770    public virtual void NotifyCollision()
 771    {
 3772        if (!IsActive) return;
 773
 1774        Turning!.NotifyCollision();
 1775    }
 776
 777    #endregion
 778
 779    #region Traversal Condition Management
 780
 781    /// <summary>
 782    /// Updates the object to a grounded state using a host-provided platform snapshot plus surface settings.
 783    /// Inert snapshots still describe the contacted surface but opt out of moving-platform carry logic.
 784    /// </summary>
 785    public virtual void SetGroundContact(
 786        Fixed64 surfaceLevel,
 787        PlatformSnapshot platform = default,
 788        Fixed64? surfaceFriction = null,
 789        MotionTransfer motionTransfer = MotionTransfer.None,
 790        Fixed64? ceilingLevel = null,
 791        bool updateMotorState = false)
 792    {
 27793        SetTrekCondition(
 27794            medium: TraversalMedium.Solid,
 27795            surfaceLevel: surfaceLevel,
 27796            surfaceCondition: new GroundCondition()
 27797            {
 27798                Platform = platform,
 27799                SurfaceFriction = surfaceFriction ?? Fixed64.Zero,
 27800                MotionTransferState = motionTransfer
 27801            },
 27802            ceilingLevel: ceilingLevel,
 27803            updateMotorState: updateMotorState);
 27804    }
 805
 806    /// <summary>
 807    /// Updates the object to an airborne state while preserving the last known ground condition unless an override is p
 808    /// </summary>
 809    public virtual void SetAirborne(
 810        Fixed64? surfaceLevel = null,
 811        GroundCondition? launchCondition = null,
 812        Fixed64? ceilingLevel = null,
 813        bool updateMotorState = false)
 814    {
 2815        SetTrekCondition(
 2816            medium: TraversalMedium.Gas,
 2817            surfaceLevel: surfaceLevel,
 2818            surfaceCondition: launchCondition,
 2819            replaceGroundContact: launchCondition.HasValue,
 2820            ceilingLevel: ceilingLevel,
 2821            updateMotorState: updateMotorState);
 2822    }
 823
 824    /// <summary>
 825    /// Updates the object to a water-contact state and clears any grounded platform contact.
 826    /// </summary>
 827    public virtual void SetWaterContact(
 828        Fixed64 surfaceLevel,
 829        Fixed64? ceilingLevel = null,
 830        bool updateMotorState = false)
 831    {
 1832        SetTrekCondition(
 1833            medium: TraversalMedium.Liquid,
 1834            surfaceLevel: surfaceLevel,
 1835            surfaceCondition: null,
 1836            ceilingLevel: ceilingLevel,
 1837            updateMotorState: updateMotorState);
 1838    }
 839
 840    /// <summary>
 841    /// Updates the scout’s traversal state, including its current medium and surface information.
 842    /// </summary>
 843    /// <remarks>
 844    /// Make sure to update this before the next <see cref="CommitFrameMotion"/> so <see cref="NavMotor.FinalizeTraversa
 845    /// If the motor must see the new snapshot before the next <see cref="Simulate"/>, either pass
 846    /// <paramref name="updateMotorState"/> as <c>true</c> or call <see cref="SyncCurrentTrekConditionToMotor"/>.
 847    /// </remarks>
 848    /// <param name="medium">The traversal medium (e.g., ground, air, water).</param>
 849    /// <param name="surfaceLevel">The vertical surface level, if applicable.</param>
 850    /// <param name="surfaceCondition">The ground state data, if applicable.</param>
 851    /// <param name="replaceGroundContact">Whether to replace the current ground contact platform.  This should be true 
 852    /// <param name="ceilingLevel">The vertical ceiling level, if applicable.</param>
 853    /// <param name="updateMotorState">Flags whether or not to update the motor's internal surface state.  Otherwise, it
 854    public virtual void SetTrekCondition(
 855        TraversalMedium? medium = null,
 856        Fixed64? surfaceLevel = null,
 857        GroundCondition? surfaceCondition = null,
 858        bool replaceGroundContact = true,
 859        Fixed64? ceilingLevel = null,
 860        bool updateMotorState = false)
 861    {
 34862        if (!IsActive)
 1863            return;
 864
 33865        _frameCondition.Medium = medium ?? _frameCondition.Medium;
 33866        _frameCondition.SurfaceLevel = surfaceLevel ?? _frameCondition.SurfaceLevel;
 33867        if (replaceGroundContact)
 29868            _frameCondition.GroundState = surfaceCondition;
 33869        _frameCondition.CeilingLevel = ceilingLevel ?? _frameCondition.CeilingLevel;
 870
 33871        if (updateMotorState)
 13872            SyncCurrentTrekConditionToMotor();
 33873    }
 874
 875    /// <summary>
 876    /// Pushes the current traversal snapshot into the motor before the next traversal phase begins.
 877    /// </summary>
 878    public virtual void SyncCurrentTrekConditionToMotor()
 879    {
 16880        SwiftThrowHelper.ThrowIfTrue(
 16881            !IsActive,
 16882            message: "Navigator must be Setup and Initialized before syncing traversal state to the motor.");
 883
 15884        Motor!.SyncTraversalState(_frameCondition);
 15885    }
 886
 887    /// <summary>
 888    /// Replaces the current traversal state with the given one.
 889    /// </summary>
 890    /// <param name="state">The new traversal condition to apply.</param>
 891    /// <param name="updateMotorState">Flags whether or not to immediately sync the new traversal snapshot into the moto
 892    public virtual void ReplaceTrekCondition(TrekCondition state, bool updateMotorState)
 893    {
 4894        SwiftThrowHelper.ThrowIfTrue(
 4895            updateMotorState && !IsActive,
 4896            message: "Navigator must be Setup and Initialized before syncing traversal state to the motor.");
 897
 3898        _frameCondition = state.Clone();
 3899        if (updateMotorState)
 1900            SyncCurrentTrekConditionToMotor();
 3901    }
 902
 903    /// <summary>
 904    /// Checks and updates the current traversal condition.
 905    /// </summary>
 906    public abstract void CheckTrekCondition();
 907
 908    #endregion
 909
 910    #region Deltas - Position / Velocity / Rotation
 911
 912    /// <summary>
 913    /// Applies a positional delta to the current position and updates the last known position accordingly.
 914    /// </summary>
 915    /// <remarks>
 916    /// This method adjusts both the current and last positions to maintain consistent velocity calculations.
 917    /// Use this method to apply external position changes without affecting velocity tracking.
 918    /// </remarks>
 919    /// <param name="delta">
 920    /// The vector representing the positional change to apply. If the vector is <see cref="Vector3d.Zero"/>, no change 
 921    /// </param>
 922    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 923    public virtual void AddPositionDelta(Vector3d delta)
 924    {
 57925        if (delta == Vector3d.Zero) return;
 926
 3927        _positionDelta += delta;
 928        // shift last position so it doesn't alter object's velocity
 3929        _lastPosition += delta;
 3930    }
 931
 932    /// <summary>
 933    /// Applies the specified rotation delta to the current rotation state.
 934    /// </summary>
 935    /// <param name="delta">
 936    /// The rotation delta to apply. Must not be <see cref="FixedQuaternion.Identity"/>; otherwise, no change is made.
 937    /// </param>
 938    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 939    public virtual void ApplyRotationDelta(FixedQuaternion delta)
 940    {
 55941        if (delta == FixedQuaternion.Identity) return;
 942
 1943        _rotationDelta *= delta;
 1944    }
 945
 946    /// <summary>
 947    /// Adds the specified velocity change to the current velocity delta.
 948    /// </summary>
 949    /// <param name="delta">The velocity change to add. If this value is <see cref="Vector3d.Zero"/>, no change is appli
 950    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 951    public virtual void AddVelocityDelta(Vector3d delta)
 952    {
 30953        if (delta == Vector3d.Zero) return;
 954
 955        // assume a mass of 1...for now
 26956        _velocityDelta += delta;
 26957    }
 958
 959    #endregion
 960
 961    #region Utilities
 962
 963    /// <summary>
 964    /// Generates a new globally unique identifier (GUID) for use in object identification or tracking.
 965    /// </summary>
 966    /// <remarks>Override this method to customize GUID generation logic if a different strategy is required by derived 
 967    /// <returns>A new <see cref="Guid"/> value that is guaranteed to be unique across space and time.</returns>
 968    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 969    protected virtual Guid GenerateGUID()
 970    {
 265971        TrailblazerWorldContext context = RequireContext();
 265972        return context.Navigation.CreateNavigatorId();
 973    }
 974
 975    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 976    private TrailblazerWorldContext EnsureContextForSetup()
 977    {
 273978        if (_context != null)
 272979            return RequireContext();
 980
 1981        throw new InvalidOperationException(
 1982            "Navigator requires a TrailblazerWorldContext before setup. Pass a context to the constructor, " +
 1983            "call BindContext(context), or use Setup(context, ...).");
 984    }
 985
 986    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 987    private TrailblazerWorldContext RequireContext()
 988    {
 1331989        SwiftThrowHelper.ThrowIfTrue(
 1331990            _context == null,
 1331991            message: "Navigator requires a TrailblazerWorldContext before simulation.");
 992
 1330993        TrailblazerWorldContext.ThrowIfUnusable(_context);
 1330994        return _context;
 995    }
 996
 997    private void RebuildCommittedCellState(bool emitChange)
 998    {
 64999        NavigationCommittedCellResolveStatus status = TryResolveCommittedCellState(
 641000            out NavigationCommittedCellState resolved);
 641001        if (status == NavigationCommittedCellResolveStatus.Unavailable)
 31002            return;
 1003
 611004        NavigationCommittedCellState? previous = _lastCommittedCell;
 611005        NavigationCommittedCellState? current = status == NavigationCommittedCellResolveStatus.Resolved
 611006            ? resolved
 611007            : null;
 611008        _lastCommittedCell = current;
 611009        _steering?.ConsumePendingCommittedAreaPolicy();
 1010
 611011        if (emitChange && !RepresentsSameCellEntry(previous, current))
 211012            CommittedCellChanged?.Invoke(current);
 501013    }
 1014
 1015    private NavigationCommittedCellResolveStatus TryResolveCommittedCellState(
 1016        out NavigationCommittedCellState state)
 1017    {
 641018        TrailblazerWorldContext context = RequireContext();
 641019        if (!NavigatorOccupancyTracker.TryResolveVoxel(
 641020                context.World,
 641021                Position,
 641022                out VoxelGrid? grid,
 641023                out Voxel? voxel))
 1024        {
 21025            state = default;
 21026            return NavigationCommittedCellResolveStatus.NoCell;
 1027        }
 1028
 621029        NavigationCommittedCellResolveStatus status = context.Pathing.TryResolveCommittedCell(
 621030            grid!.Configuration.ToGridKey(),
 621031            voxel!.WorldIndex,
 621032            out NavigationCellAddress address,
 621033            out NavigationAreaId area,
 621034            out long graphVersion);
 621035        if (status != NavigationCommittedCellResolveStatus.Resolved)
 1036        {
 291037            state = default;
 291038            return status;
 1039        }
 1040
 331041        state = new NavigationCommittedCellState(
 331042            address,
 331043            area,
 331044            _frameCondition.Medium,
 331045            graphVersion,
 331046            _steering?.AreaPolicyForCommit,
 331047            context.FrameCount);
 331048        return NavigationCommittedCellResolveStatus.Resolved;
 1049    }
 1050
 1051    private static bool RepresentsSameCellEntry(
 1052        NavigationCommittedCellState? first,
 1053        NavigationCommittedCellState? second)
 1054    {
 321055        if (!first.HasValue || !second.HasValue)
 261056            return first.HasValue == second.HasValue;
 1057
 61058        NavigationCommittedCellState left = first.Value;
 61059        NavigationCommittedCellState right = second.Value;
 61060        return left.Address == right.Address
 61061            && left.Area == right.Area
 61062            && left.Medium == right.Medium;
 1063    }
 1064
 1065    private void ValidateGuidedSurfaceQuery(PathQuery query)
 1066    {
 1041067        SwiftThrowHelper.ThrowIfArgument(
 1041068            query.Agent != NavigationProfile
 1041069            || query.Start.Position != FootPosition
 1041070            || query.Traversal.StartMedium != _frameCondition.Medium,
 1041071            paramName: nameof(query),
 1041072            message: "Guided queries must use the Navigator profile, current foot position, and current traversal medium
 1011073    }
 1074
 1075    private void ApplyTransitionLocomotionHints(
 1076        TraversalTransitionLocomotionHints hints,
 1077        bool pending)
 1078    {
 301079        _frameRequest.IsRequestingClimb = pending
 301080            ? (hints & TraversalTransitionLocomotionHints.RequestClimb) != 0
 301081            : (hints & TraversalTransitionLocomotionHints.PreserveClimbAfterCompletion) != 0;
 301082    }
 1083
 1084    #endregion
 1085
 1086    #region Occupancy Mangement
 1087
 1088    /// <summary>
 1089    /// Checks and updates the occupancy status of the current and previous voxels based on the object's position.
 1090    /// </summary>
 1091    /// <remarks>
 1092    /// This method ensures that the object is registered as an occupant in the correct voxel and
 1093    /// removed from the previous voxel if the position has changed.
 1094    /// It should be called whenever the object's position may have changed to maintain accurate occupancy tracking.
 1095    /// </remarks>
 1096    /// <param name="init">Indicates whether the occupancy check is being performed during initialization.
 1097    /// If set to <see langword="true"/>, the check is performed regardless of position changes.
 1098    /// </param>
 1099    protected virtual void CheckVoxelOccupancy(bool init = false)
 1100    {
 3151101        NavigatorOccupancyTracker.Update(
 3151102            RequireContext().World,
 3151103            this,
 3151104            Position,
 3151105            init ? Position : LastPosition,
 3151106            init);
 3151107    }
 1108
 1109    #endregion
 1110}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Navigator/Navigator.HeightmapGrounding.cs

#LineLine coverage
 1//=======================================================================
 2// Navigator.HeightmapGrounding.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 FixedMathSharp;
 9using Trailblazer.Heightmaps;
 10using Trailblazer.Navigation.Motor;
 11
 12namespace Trailblazer.Navigation;
 13
 14public abstract partial class Navigator
 15{
 16    #region Heightmap Grounding
 17
 18    /// <summary>
 19    /// Applies configured heightmap grounding when a concrete navigator chooses to opt in.
 20    /// </summary>
 21    /// <remarks>
 22    /// The base navigator never calls this automatically. Host/concrete navigators should invoke it
 23    /// from their traversal probing code after they know the navigator is grounded on solid terrain.
 24    /// </remarks>
 25    /// <param name="updateMotorState">Whether to immediately sync the resulting ground contact into the motor.</param>
 26    /// <param name="surfaceFriction">Optional friction stored in the generated ground condition.</param>
 27    /// <param name="motionTransfer">Motion transfer mode stored in the generated ground condition.</param>
 28    /// <returns>True when a registered heightmap sample updated this navigator's ground contact.</returns>
 29    protected bool TryApplyHeightmapGrounding(
 30        bool updateMotorState = false,
 31        Fixed64? surfaceFriction = null,
 32        MotionTransfer motionTransfer = MotionTransfer.None)
 33    {
 2234        if (!IsActive || _heightmapGrounding.Mode == HeightmapGroundingMode.Disabled)
 235            return false;
 2036        if (_frameCondition.Medium != TraversalMedium.Solid)
 237            return false;
 38
 1839        TrailblazerWorldContext context = RequireContext();
 1840        Vector3d queryPosition = GetHeightmapGroundingQueryPosition();
 1841        string? preferredLayerName = _heightmapGrounding.ActiveLayerName ?? _heightmapGrounding.LayerName;
 1842        if (!context.Heightmaps.TrySampleGround(queryPosition, preferredLayerName, out HeightmapSample sample))
 43        {
 244            _heightmapGrounding.ActiveLayerName = null;
 245            return false;
 46        }
 47
 1648        _heightmapGrounding.ActiveLayerName = sample.LayerName;
 1649        SetGroundContact(
 1650            surfaceLevel: sample.GroundY,
 1651            surfaceFriction: surfaceFriction,
 1652            motionTransfer: motionTransfer,
 1653            updateMotorState: updateMotorState);
 54
 1655        if (_heightmapGrounding.Mode == HeightmapGroundingMode.SurfaceLevelAndPosition)
 1356            TryProjectRootToHeightmapSample(sample);
 57
 1658        return true;
 59    }
 60
 61    private Vector3d GetHeightmapGroundingQueryPosition()
 62    {
 1863        return new Vector3d(
 1864            _position.X,
 1865            _position.Y - BodyShape.RootToFootOffsetY - _heightmapGrounding.GroundOffset,
 1866            _position.Z);
 67    }
 68
 69    private void TryProjectRootToHeightmapSample(HeightmapSample sample)
 70    {
 1371        Fixed64 targetRootY = sample.GroundY + BodyShape.RootToFootOffsetY + _heightmapGrounding.GroundOffset;
 1372        Fixed64 correctionY = targetRootY - _position.Y;
 1373        if (correctionY == Fixed64.Zero)
 474            return;
 75
 976        Fixed64? snapTolerance = _heightmapGrounding.SnapTolerance;
 977        if (snapTolerance.HasValue && correctionY.Abs() > snapTolerance.Value)
 178            return;
 79
 880        ProjectRootWithoutVelocity(new Vector3d(Fixed64.Zero, correctionY, Fixed64.Zero));
 881    }
 82
 83    private void ProjectRootWithoutVelocity(Vector3d correction)
 84    {
 885        Vector3d oldPosition = _position;
 886        _position += correction;
 887        _lastPosition += correction;
 888        UpdateVoxelOccupancyAfterRootProjection(oldPosition, _position);
 889    }
 90
 91    private void UpdateVoxelOccupancyAfterRootProjection(Vector3d oldPosition, Vector3d newPosition)
 92    {
 893        NavigatorOccupancyTracker.UpdateAfterRootProjection(
 894            _context!.World, this, oldPosition, newPosition);
 895    }
 96
 97    #endregion
 98}

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Navigator/Navigator.Serialization.cs

#LineLine coverage
 1//=======================================================================
 2// Navigator.Serialization.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 Chronicler;
 10using FixedMathSharp;
 11using GridForge.Grids;
 12using Trailblazer.Navigation.Motor;
 13using Trailblazer.Navigation.Steering;
 14using Trailblazer.Navigation.Turning;
 15using Trailblazer.Pathing;
 16
 17namespace Trailblazer.Navigation;
 18
 19public abstract partial class Navigator
 20{
 21    private const int CurrentSerializationSchemaVersion = 3;
 22
 23    #region Serialization
 24
 25    /// <inheritdoc />
 26    public virtual void RecordData(IChronicler chronicler)
 27    {
 15328        bool isLoading = chronicler.Mode == SerializationMode.Loading;
 15329        int schemaVersion = isLoading ? 0 : CurrentSerializationSchemaVersion;
 15330        var navigationProfileRecord = isLoading
 15331            ? new NavigationAgentProfileRecord()
 15332            : new NavigationAgentProfileRecord(_navigationProfile);
 15333        Vector3d position = isLoading ? Vector3d.Zero : _position;
 15334        TrekCondition frameCondition = isLoading ? new TrekCondition() : _frameCondition.Clone();
 15335        var pathSession = new NavigatorPathSessionRecord();
 15336        if (!isLoading)
 7737            pathSession.Capture(_steering?.CurrentQuery);
 38
 39        // Validate the exact schema, shell profile, restored frame, and durable
 40        // query before any state on the existing runtime shell is changed.
 15341        RecordValues.Look(chronicler, ref schemaVersion, "SchemaVersion", 0);
 15342        RecordDeep.Look(chronicler, ref navigationProfileRecord, "NavigationProfile");
 15343        RecordValues.Look(chronicler, ref position, "Position", Vector3d.Zero);
 15344        RecordDeepStruct.Look(chronicler, ref frameCondition, "FrameCondition");
 15345        RecordDeep.Look(chronicler, ref pathSession, "PathSession");
 46
 15347        PathQuery? restoredQuery = null;
 15348        if (isLoading)
 49        {
 7650            SwiftThrowHelper.ThrowIfTrue(
 7651                schemaVersion != CurrentSerializationSchemaVersion,
 7652                message: "Serialized Navigator schema is missing, retired, or unsupported.");
 7053            SwiftThrowHelper.ThrowIfTrue(
 7054                !navigationProfileRecord.TryCreate(out NavigationAgentProfile recordedProfile)
 7055                || recordedProfile != _navigationProfile,
 7056                message: "Serialized navigation profile must exactly match the configured Navigator shell.");
 6657            SwiftThrowHelper.ThrowIfTrue(
 6658                !TraversalTransitionDefinition.IsKnownMedium(frameCondition.Medium),
 6659                message: "Serialized Navigator traversal medium is missing or unsupported.");
 60
 6461            Vector3d loadedFootPosition =
 6462                position + Vector3d.Down * _navigationProfile.Shape.RootToFootOffsetY;
 6463            SwiftThrowHelper.ThrowIfTrue(
 6464                !pathSession.TryCreateQuery(
 6465                    loadedFootPosition,
 6466                    frameCondition.Medium,
 6467                    _navigationProfile,
 6468                    out restoredQuery)
 6469                || (restoredQuery.HasValue && _steering == null),
 6470                message: "Serialized Navigator path session is missing, invalid, or unsupported.");
 71
 5072            ValidateNestedRecords(chronicler, frameCondition);
 73        }
 74
 11075        Vector3d lastPosition = isLoading ? Vector3d.Zero : _lastPosition;
 11076        FixedQuaternion rotation = isLoading ? FixedQuaternion.Identity : _rotation;
 11077        Vector3d velocity = isLoading ? Vector3d.Zero : _velocity;
 11078        Fixed64 speed = isLoading ? Fixed64.Zero : _speed;
 11079        Vector3d acceleration = isLoading ? Vector3d.Zero : _acceleration;
 11080        Guid globalId = isLoading ? Guid.Empty : _globalId;
 11081        byte occupantGroupId = isLoading ? (byte)1 : _occupantGroupId;
 11082        bool isLockedOn = !isLoading && _isLockedOn;
 11083        Fixed64 stuckThresholdSpeed = isLoading ? Fixed64.Zero : _stuckThresholdSpeed;
 11084        bool isGuided = !isLoading && _isGuided;
 11085        TrekRequest frameRequest = isLoading ? new TrekRequest() : _frameRequest.Clone();
 11086        NavigatorHeightmapGroundingSettings heightmapGrounding = isLoading
 11087            ? new NavigatorHeightmapGroundingSettings()
 11088            : _heightmapGrounding;
 89
 11090        RecordValues.Look(chronicler, ref lastPosition, "LastPosition", Vector3d.Zero);
 11091        RecordValues.Look(chronicler, ref rotation, "Rotation", FixedQuaternion.Identity);
 11092        RecordValues.Look(chronicler, ref velocity, "Velocity", Vector3d.Zero);
 11093        RecordValues.Look(chronicler, ref speed, "Speed", Fixed64.Zero);
 11094        RecordValues.Look(chronicler, ref acceleration, "Acceleration", Vector3d.Zero);
 11095        RecordValues.Look(chronicler, ref globalId, "GlobalId", Guid.Empty);
 11096        RecordValues.Look(chronicler, ref occupantGroupId, "OccupantGroupId", (byte)1);
 11097        RecordValues.Look(chronicler, ref isLockedOn, "IsLockedOn", false);
 11098        RecordValues.Look(chronicler, ref stuckThresholdSpeed, "StuckThresholdSpeed", Fixed64.Zero);
 11099        RecordValues.Look(chronicler, ref isGuided, "IsGuided", false);
 110100        RecordDeepStruct.Look(chronicler, ref frameRequest, "FrameRequest");
 110101        RecordDeep.Look(chronicler, ref heightmapGrounding, "HeightmapGrounding");
 102
 108103        if (isLoading)
 104        {
 31105            GridOccupantManager.TryDeregister(RequireContext().World, this);
 31106            _position = position;
 31107            _lastPosition = lastPosition;
 31108            _rotation = rotation;
 31109            _velocity = velocity;
 31110            _speed = speed;
 31111            _acceleration = acceleration;
 31112            _globalId = globalId;
 31113            _occupantGroupId = occupantGroupId;
 31114            _isLockedOn = isLockedOn;
 31115            _stuckThresholdSpeed = stuckThresholdSpeed;
 31116            _isGuided = isGuided;
 31117            _frameCondition = frameCondition;
 31118            _frameRequest = frameRequest;
 31119            _heightmapGrounding = heightmapGrounding;
 120        }
 121
 108122        if (_steering != null)
 102123            RecordDeep.Look(chronicler, ref _steering, "Steering");
 108124        if (_turning != null)
 102125            RecordDeep.Look(chronicler, ref _turning, "Turning");
 108126        if (_motor != null)
 102127            RecordDeep.Look(chronicler, ref _motor, "Motor");
 128
 108129        if (isLoading)
 130        {
 31131            TrailblazerWorldContext context = RequireContext();
 31132            _pendingTransition = null;
 31133            _steering?.RestoreQueryFromLoadedSession(restoredQuery);
 134
 31135            Forward = Rotation != FixedQuaternion.Identity
 31136                ? Rotation.Rotate(Vector3d.Forward)
 31137                : Vector3d.Forward;
 138
 31139            _positionDelta = Vector3d.Zero;
 31140            _velocityDelta = Vector3d.Zero;
 31141            _rotationDelta = FixedQuaternion.Identity;
 31142            _isSet = true;
 31143            _isInitialized = Motor != null;
 144
 31145            _steering?.BindContext(context);
 31146            _steering?.BindPendingTransitionOwner(this);
 147
 31148            _motor?.BindContext(context);
 149
 31150            _turning?.BindContext(context);
 31151            _turning?.OnInitialize(Radius);
 152
 31153            CheckVoxelOccupancy(init: true);
 31154            _lastCommittedCell = null;
 31155            RebuildCommittedCellState(emitChange: false);
 156        }
 108157    }
 158
 159    private void ValidateNestedRecords(IChronicler chronicler, TrekCondition frameCondition)
 160    {
 50161        TrailblazerWorldContext context = RequireContext();
 50162        if (_steering != null)
 163        {
 47164            var steering = new NavSteering(context);
 47165            RecordDeep.Look(chronicler, ref steering, "Steering");
 47166            SwiftThrowHelper.ThrowIfTrue(
 47167                steering.BrakingPower < Fixed64.Zero
 47168                || steering.WaypointTolerance < Fixed64.Zero
 47169                || steering.GroupFactor < Fixed64.Zero
 47170                || steering.AvoidFactor < Fixed64.Zero,
 47171                message: "Serialized steering limits must be non-negative.");
 172        }
 173
 44174        if (_turning != null)
 175        {
 41176            var turning = new NavTurning(context, Radius);
 41177            RecordDeep.Look(chronicler, ref turning, "Turning");
 41178            SwiftThrowHelper.ThrowIfTrue(
 41179                turning.TurnRate < Fixed64.Zero,
 41180                message: "Serialized turn rate must be non-negative.");
 181        }
 182
 43183        if (_motor != null)
 184        {
 40185            var motor = new NavMotor(
 40186                context,
 40187                frameCondition,
 40188                CreateLocomotionProfile());
 40189            RecordDeep.Look(chronicler, ref motor, "Motor");
 40190            SwiftThrowHelper.ThrowIfTrue(
 40191                motor.Handler.Move.MaxSlowSpeed < Fixed64.Zero
 40192                || motor.Handler.Move.MaxModerateSpeed < Fixed64.Zero
 40193                || motor.Handler.Move.MaxFastSpeed < Fixed64.Zero
 40194                || motor.Handler.Move.MaxSidewaysSpeed < Fixed64.Zero
 40195                || motor.Handler.Move.MaxBackwardsSpeed < Fixed64.Zero
 40196                || motor.Handler.Move.MaxGroundAcceleration < Fixed64.Zero
 40197                || motor.Handler.Move.MaxAirAcceleration < Fixed64.Zero,
 40198                message: "Serialized movement limits must be non-negative.");
 199        }
 33200    }
 201
 202    #endregion
 203}

Methods/Properties

.ctor()
get_Position()
get_LastPosition()
get_Rotation()
get_Velocity()
get_Speed()
get_Acceleration()
get_StuckThresholdSpeed()
get_IsActive()
get_Context()
get_Steering()
get_Turning()
get_Motor()
get_IsGuided()
get_NavigationProfile()
get_BodyShape()
get_Radius()
get_FootPosition()
get_HeightmapGrounding()
get_IsLockedOn()
get_PendingTransition()
get_LastCommittedCell()
get_GlobalId()
get_OccupantGroupId()
.ctor(Trailblazer.TrailblazerWorldContext)
BindContext(Trailblazer.TrailblazerWorldContext)
Activate(Trailblazer.TrailblazerWorldContext,Trailblazer.Navigation.Motor.TrekCondition,FixedMathSharp.Vector3d,Trailblazer.Pathing.NavigationAgentProfile,System.Nullable`1<FixedMathSharp.FixedQuaternion>,System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<System.Guid>)
Activate(Trailblazer.Navigation.Motor.TrekCondition,FixedMathSharp.Vector3d,Trailblazer.Pathing.NavigationAgentProfile,System.Nullable`1<FixedMathSharp.FixedQuaternion>,System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<System.Guid>)
Setup(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,Trailblazer.Pathing.NavigationAgentProfile,System.Nullable`1<FixedMathSharp.FixedQuaternion>,System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<System.Guid>)
Setup(FixedMathSharp.Vector3d,Trailblazer.Pathing.NavigationAgentProfile,System.Nullable`1<FixedMathSharp.FixedQuaternion>,System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<System.Guid>)
Initialize(Trailblazer.Navigation.Motor.TrekCondition)
CreateLocomotionProfile()
Reset()
PrewarmMovementGroup()
ApplyInputTrekRequest(System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<Trailblazer.Navigation.TrekRate>,System.Nullable`1<FixedMathSharp.Vector3d>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Boolean)
ApplyGuidedTrekRequest(Trailblazer.Pathing.PathQuery,System.Nullable`1<Trailblazer.Navigation.TrekRate>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Nullable`1<System.Boolean>,System.Boolean,System.Int32)
ConfigureHeightmapGrounding(Trailblazer.Navigation.HeightmapGroundingMode,System.String,System.Nullable`1<FixedMathSharp.Fixed64>,System.Nullable`1<FixedMathSharp.Fixed64>)
SetFrameJumpAffordability(System.Boolean)
ToggleGuidedJump(System.Boolean)
ToggleGuidedFlight(System.Boolean)
ToggleGuidedSwim(System.Boolean)
ToggleGuidedClimb(System.Boolean)
CompletePendingTransition(System.Runtime.InteropServices.InAttribute))
NotifySteeringSessionEnded()
SetGuidedTrekRate(Trailblazer.Navigation.TrekRate)
TryGetTurnDirection(Trailblazer.Navigation.Motor.TrekRequest,FixedMathSharp.Vector3d&)
Simulate()
CommitFrameMotion()
NotifyCollision()
SetGroundContact(FixedMathSharp.Fixed64,Trailblazer.Navigation.Motor.PlatformSnapshot,System.Nullable`1<FixedMathSharp.Fixed64>,Trailblazer.Navigation.Motor.MotionTransfer,System.Nullable`1<FixedMathSharp.Fixed64>,System.Boolean)
SetAirborne(System.Nullable`1<FixedMathSharp.Fixed64>,System.Nullable`1<Trailblazer.Navigation.Motor.GroundCondition>,System.Nullable`1<FixedMathSharp.Fixed64>,System.Boolean)
SetWaterContact(FixedMathSharp.Fixed64,System.Nullable`1<FixedMathSharp.Fixed64>,System.Boolean)
SetTrekCondition(System.Nullable`1<Trailblazer.TraversalMedium>,System.Nullable`1<FixedMathSharp.Fixed64>,System.Nullable`1<Trailblazer.Navigation.Motor.GroundCondition>,System.Boolean,System.Nullable`1<FixedMathSharp.Fixed64>,System.Boolean)
SyncCurrentTrekConditionToMotor()
ReplaceTrekCondition(Trailblazer.Navigation.Motor.TrekCondition,System.Boolean)
AddPositionDelta(FixedMathSharp.Vector3d)
ApplyRotationDelta(FixedMathSharp.FixedQuaternion)
AddVelocityDelta(FixedMathSharp.Vector3d)
GenerateGUID()
EnsureContextForSetup()
RequireContext()
RebuildCommittedCellState(System.Boolean)
TryResolveCommittedCellState(Trailblazer.Navigation.NavigationCommittedCellState&)
RepresentsSameCellEntry(System.Nullable`1<Trailblazer.Navigation.NavigationCommittedCellState>,System.Nullable`1<Trailblazer.Navigation.NavigationCommittedCellState>)
ValidateGuidedSurfaceQuery(Trailblazer.Pathing.PathQuery)
ApplyTransitionLocomotionHints(Trailblazer.Pathing.TraversalTransitionLocomotionHints,System.Boolean)
CheckVoxelOccupancy(System.Boolean)
TryApplyHeightmapGrounding(System.Boolean,System.Nullable`1<FixedMathSharp.Fixed64>,Trailblazer.Navigation.Motor.MotionTransfer)
GetHeightmapGroundingQueryPosition()
TryProjectRootToHeightmapSample(Trailblazer.Heightmaps.HeightmapSample)
ProjectRootWithoutVelocity(FixedMathSharp.Vector3d)
UpdateVoxelOccupancyAfterRootProjection(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
RecordData(Chronicler.IChronicler)
ValidateNestedRecords(Chronicler.IChronicler,Trailblazer.Navigation.Motor.TrekCondition)