< Summary

Information
Class: Trailblazer.TrailblazerWorldContext
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Runtime/TrailblazerWorldContext.cs
Line coverage
96%
Covered lines: 108
Uncovered lines: 4
Coverable lines: 112
Total lines: 382
Line coverage: 96.4%
Branch coverage
73%
Covered branches: 19
Total branches: 26
Branch coverage: 73%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_IsDisposed()100%11100%
get_VoxelSize()100%11100%
get_FrameRate()100%11100%
get_DeltaTime()100%11100%
get_InvDeltaTime()100%11100%
get_FrameCount()100%11100%
get_TotalTime()100%11100%
get_AccumulatedTime()100%11100%
get_ResetAccumulation()100%11100%
get_ExpectedAccumulation()100%11100%
Attach(...)100%44100%
CreateOwned(...)100%11100%
Simulate()100%11100%
LateSimulate()100%11100%
Visualize()100%11100%
Reset()100%11100%
SetFrameRate(...)100%11100%
GetFrameFromTime(...)100%11100%
RegisterOnSimulate(...)100%11100%
RegisterOnLateSimulate(...)100%11100%
RegisterOnVisualize(...)100%11100%
RegisterOnReset(...)100%11100%
RegisterOnFrameRateChanged(...)100%11100%
Dispose()100%66100%
CreateRegistered(...)100%11100%
ThrowIfWorldOwned(...)62.5%9875%
ReleaseWorldOwnership(...)50%6683.33%
ThrowIfDisposed()50%2275%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Runtime/TrailblazerWorldContext.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using SwiftCollections;
 4using System;
 5using Trailblazer.Heightmaps;
 6using Trailblazer.Navigation;
 7using Trailblazer.Pathing;
 8
 9namespace Trailblazer;
 10
 11/// <summary>
 12/// Owns Trailblazer runtime state for one explicit <see cref="GridWorld"/>.
 13/// </summary>
 14/// <remarks>
 15/// This is the context-first host API for multi-world Trailblazer usage. It owns world lifetime,
 16/// deterministic clock state, pathing state, transition state, volume rules, reachability snapshots,
 17/// and guide caches. Later phases move navigation coordination state behind this context.
 18/// </remarks>
 19public sealed class TrailblazerWorldContext : IDisposable
 20{
 121    private static readonly object _worldOwnershipLock = new();
 22
 123    private static readonly SwiftDictionary<GridWorld, WeakReference<TrailblazerWorldContext>> _worldOwners = new();
 24
 96125    private readonly TrailblazerClock _clock = new();
 26
 96127    private readonly TrailblazerLifecycleHooks _hooks = new();
 28
 29    private readonly bool _ownsWorld;
 30
 31    private bool _disposed;
 32
 96133    private TrailblazerWorldContext(GridWorld world, bool ownsWorld)
 34    {
 96135        World = world;
 96136        _ownsWorld = ownsWorld;
 96137        Pathing = new TrailblazerPathingService(this);
 96138        Transitions = new TrailblazerTransitionService(this, Pathing.State);
 96139        VolumeRules = new TrailblazerVolumeRulesService(this, Pathing.State);
 96140        Guides = new TrailblazerGuideService(this, Pathing.State);
 96141        Navigation = new TrailblazerNavigationService(this);
 96142        Heightmaps = new TrailblazerHeightmapService(this);
 96143    }
 44
 45    /// <summary>
 46    /// Gets the explicit GridForge world owned or referenced by this context.
 47    /// </summary>
 48    public GridWorld World { get; }
 49
 50    /// <summary>
 51    /// Gets this context's world-local pathing service.
 52    /// </summary>
 53    public TrailblazerPathingService Pathing { get; }
 54
 55    /// <summary>
 56    /// Gets this context's world-local traversal transition service.
 57    /// </summary>
 58    public TrailblazerTransitionService Transitions { get; }
 59
 60    /// <summary>
 61    /// Gets this context's world-local raw-volume medium rule service.
 62    /// </summary>
 63    public TrailblazerVolumeRulesService VolumeRules { get; }
 64
 65    /// <summary>
 66    /// Gets this context's world-local path guide service.
 67    /// </summary>
 68    public TrailblazerGuideService Guides { get; }
 69
 70    /// <summary>
 71    /// Gets this context's world-local navigation coordination service.
 72    /// </summary>
 73    public TrailblazerNavigationService Navigation { get; }
 74
 75    /// <summary>
 76    /// Gets this context's world-local heightmap registry and sampling service.
 77    /// </summary>
 78    public TrailblazerHeightmapService Heightmaps { get; }
 79
 80    /// <summary>
 81    /// Gets whether this context has been disposed.
 82    /// </summary>
 4007583    public bool IsDisposed => _disposed;
 84
 85    /// <summary>
 86    /// Gets the voxel size of this context's world.
 87    /// </summary>
 88    public Fixed64 VoxelSize
 89    {
 90        get
 91        {
 851892            ThrowIfDisposed();
 851893            return World.VoxelSize;
 94        }
 95    }
 96
 97    /// <summary>
 98    /// Gets this context's fixed simulation frame rate.
 99    /// </summary>
 100    public int FrameRate
 101    {
 102        get
 103        {
 78104            ThrowIfDisposed();
 78105            return _clock.FrameRate;
 106        }
 107    }
 108
 109    /// <summary>
 110    /// Gets this context's fixed simulation time step.
 111    /// </summary>
 112    public Fixed64 DeltaTime
 113    {
 114        get
 115        {
 6178116            ThrowIfDisposed();
 6178117            return _clock.DeltaTime;
 118        }
 119    }
 120
 121    /// <summary>
 122    /// Gets the reciprocal of this context's fixed simulation time step.
 123    /// </summary>
 124    public Fixed64 InvDeltaTime
 125    {
 126        get
 127        {
 4388128            ThrowIfDisposed();
 4388129            return _clock.InvDeltaTime;
 130        }
 131    }
 132
 133    /// <summary>
 134    /// Gets this context's simulated frame count.
 135    /// </summary>
 136    public int FrameCount
 137    {
 138        get
 139        {
 6845140            ThrowIfDisposed();
 6845141            return _clock.FrameCount;
 142        }
 143    }
 144
 145    /// <summary>
 146    /// Gets this context's total simulated time.
 147    /// </summary>
 148    public Fixed64 TotalTime
 149    {
 150        get
 151        {
 58152            ThrowIfDisposed();
 58153            return _clock.TotalTime;
 154        }
 155    }
 156
 157    /// <summary>
 158    /// Gets this context's accumulated visualization time.
 159    /// </summary>
 160    public Fixed64 AccumulatedTime
 161    {
 162        get
 163        {
 3164            ThrowIfDisposed();
 3165            return _clock.AccumulatedTime;
 166        }
 167    }
 168
 169    /// <summary>
 170    /// Gets whether this context's visualization accumulation will reset on the next visualize call.
 171    /// </summary>
 172    public bool ResetAccumulation
 173    {
 174        get
 175        {
 4176            ThrowIfDisposed();
 4177            return _clock.ResetAccumulation;
 178        }
 179    }
 180
 181    /// <summary>
 182    /// Gets this context's visualization accumulation expressed in simulation frames.
 183    /// </summary>
 184    public Fixed64 ExpectedAccumulation
 185    {
 186        get
 187        {
 3188            ThrowIfDisposed();
 3189            return _clock.ExpectedAccumulation;
 190        }
 191    }
 192
 193    /// <summary>
 194    /// Attaches a context to a host-owned <see cref="GridWorld"/>.
 195    /// </summary>
 196    /// <param name="world">The active world to bind.</param>
 197    /// <param name="takeOwnership">True when disposing this context should dispose the supplied world.</param>
 198    /// <returns>A context bound to <paramref name="world"/>.</returns>
 199    public static TrailblazerWorldContext Attach(GridWorld world, bool takeOwnership = false)
 200    {
 11201        if (world == null)
 1202            throw new ArgumentNullException(nameof(world));
 10203        if (!world.IsActive)
 1204            throw new InvalidOperationException("TrailblazerWorldContext requires an active GridWorld.");
 205
 9206        return CreateRegistered(world, takeOwnership);
 207    }
 208
 209    /// <summary>
 210    /// Creates a context with an owned <see cref="GridWorld"/>.
 211    /// </summary>
 212    /// <param name="voxelSize">Optional voxel size for the created world.</param>
 213    /// <param name="spatialGridCellSize">Spatial hash cell size for the created world.</param>
 214    /// <returns>A context that owns its created world.</returns>
 215    public static TrailblazerWorldContext CreateOwned(
 216        Fixed64? voxelSize = null,
 217        int spatialGridCellSize = GridWorld.DefaultSpatialGridCellSize)
 218    {
 953219        return CreateRegistered(
 953220            new GridWorld(voxelSize, spatialGridCellSize),
 953221            ownsWorld: true);
 222    }
 223
 224    /// <summary>
 225    /// Advances this context's deterministic simulation clock and ordered simulate hooks.
 226    /// </summary>
 227    public void Simulate()
 228    {
 2299229        ThrowIfDisposed();
 2299230        _clock.Simulate();
 2299231        Pathing.FlushPendingGridChanges();
 2299232        Guides.CullExpiredGuides(_clock.FrameCount);
 2299233        _hooks.InvokeSimulate();
 2299234    }
 235
 236    /// <summary>
 237    /// Runs this context's late-simulation step.
 238    /// </summary>
 239    public void LateSimulate()
 240    {
 2241        ThrowIfDisposed();
 2242        _clock.LateSimulate();
 2243        _hooks.InvokeLateSimulate();
 2244    }
 245
 246    /// <summary>
 247    /// Runs this context's visualization accumulation step.
 248    /// </summary>
 249    public void Visualize()
 250    {
 5251        ThrowIfDisposed();
 5252        _clock.Visualize();
 5253        _hooks.InvokeVisualize();
 5254    }
 255
 256    /// <summary>
 257    /// Resets this context's deterministic clock and context-local lifecycle hooks.
 258    /// </summary>
 259    public void Reset()
 260    {
 25261        ThrowIfDisposed();
 25262        _clock.Reset();
 25263        Navigation.Reset();
 25264        Heightmaps.Reset();
 25265        _hooks.InvokeReset();
 25266    }
 267
 268    /// <summary>
 269    /// Updates this context's fixed simulation frame rate.
 270    /// </summary>
 271    /// <param name="frameRate">The new frame rate. Must be greater than zero.</param>
 272    public void SetFrameRate(int frameRate)
 273    {
 11274        ThrowIfDisposed();
 11275        _clock.SetFrameRate(frameRate);
 9276        _hooks.InvokeFrameRateChanged();
 9277    }
 278
 279    /// <summary>
 280    /// Calculates the frame index containing the specified fixed-point timestamp.
 281    /// </summary>
 282    /// <param name="timestamp">The timestamp to resolve.</param>
 283    /// <returns>The zero-based frame index for the timestamp.</returns>
 284    public int GetFrameFromTime(Fixed64 timestamp)
 285    {
 1286        ThrowIfDisposed();
 1287        return _clock.GetFrameFromTime(timestamp);
 288    }
 289
 290    internal IDisposable RegisterOnSimulate(string owner, int order, Action callback)
 291    {
 7292        ThrowIfDisposed();
 7293        return _hooks.RegisterOnSimulate(owner, order, callback);
 294    }
 295
 296    internal IDisposable RegisterOnLateSimulate(string owner, int order, Action callback)
 297    {
 1298        ThrowIfDisposed();
 1299        return _hooks.RegisterOnLateSimulate(owner, order, callback);
 300    }
 301
 302    internal IDisposable RegisterOnVisualize(string owner, int order, Action callback)
 303    {
 1304        ThrowIfDisposed();
 1305        return _hooks.RegisterOnVisualize(owner, order, callback);
 306    }
 307
 308    internal IDisposable RegisterOnReset(string owner, int order, Action callback)
 309    {
 2310        ThrowIfDisposed();
 2311        return _hooks.RegisterOnReset(owner, order, callback);
 312    }
 313
 314    internal IDisposable RegisterOnFrameRateChanged(string owner, int order, Action callback)
 315    {
 1316        ThrowIfDisposed();
 1317        return _hooks.RegisterOnFrameRateChanged(owner, order, callback);
 318    }
 319
 320    /// <inheritdoc/>
 321    public void Dispose()
 322    {
 967323        lock (_worldOwnershipLock)
 324        {
 967325            if (_disposed)
 6326                return;
 327
 961328            Pathing.Dispose();
 961329            Heightmaps.Dispose();
 961330            _disposed = true;
 961331            ReleaseWorldOwnership(this);
 332
 961333            if (_ownsWorld && World.IsActive)
 956334                World.Dispose();
 961335        }
 967336    }
 337
 338    private static TrailblazerWorldContext CreateRegistered(GridWorld world, bool ownsWorld)
 339    {
 962340        lock (_worldOwnershipLock)
 341        {
 962342            ThrowIfWorldOwned(world);
 961343            TrailblazerWorldContext context = new(world, ownsWorld);
 961344            _worldOwners[world] = new WeakReference<TrailblazerWorldContext>(context);
 961345            return context;
 346        }
 961347    }
 348
 349    private static void ThrowIfWorldOwned(GridWorld world)
 350    {
 962351        if (!_worldOwners.TryGetValue(world, out WeakReference<TrailblazerWorldContext> weakOwner))
 961352            return;
 353
 1354        if (weakOwner.TryGetTarget(out TrailblazerWorldContext? owner)
 1355            && !owner.IsDisposed
 1356            && owner.World.IsActive)
 357        {
 1358            throw new InvalidOperationException("GridWorld is already attached to an active TrailblazerWorldContext.");
 359        }
 360
 0361        _worldOwners.Remove(world);
 0362    }
 363
 364    private static void ReleaseWorldOwnership(TrailblazerWorldContext context)
 365    {
 961366        if (!_worldOwners.TryGetValue(context.World, out WeakReference<TrailblazerWorldContext> weakOwner))
 0367            return;
 368
 961369        if (!weakOwner.TryGetTarget(out TrailblazerWorldContext? owner)
 961370            || ReferenceEquals(owner, context))
 371        {
 961372            _worldOwners.Remove(context.World);
 373        }
 961374    }
 375
 376    private void ThrowIfDisposed()
 377    {
 28430378        SwiftThrowHelper.ThrowIfDisposed(_disposed, nameof(TrailblazerWorldContext));
 28430379        if (!World.IsActive)
 0380            throw new InvalidOperationException("TrailblazerWorldContext is bound to an inactive GridWorld.");
 28430381    }
 382}