]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Optimize & clean up RadiationSystem (#34459)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sat, 18 Jan 2025 01:07:20 +0000 (12:07 +1100)
committerGitHub <noreply@github.com>
Sat, 18 Jan 2025 01:07:20 +0000 (12:07 +1100)
* Optimize & clean up RadiationSystem

* comments

* Update Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>
---------

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>
Content.Client/Radiation/Systems/RadiationSystem.cs
Content.Server/Radiation/Systems/RadiationSystem.Debug.cs
Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs
Content.Server/Radiation/Systems/RadiationSystem.cs
Content.Shared/Radiation/Events/OnIrradiatedEvent.cs
Content.Shared/Radiation/Events/OnRadiationOverlayUpdateEvent.cs
Content.Shared/Radiation/RadiationRay.cs

index 929ad6aa4ac58e79b6828aa487aaf8e974238728..f4f109adc7c3633ebe0c2e4029b782b0bb63b2c8 100644 (file)
@@ -9,7 +9,7 @@ public sealed class RadiationSystem : EntitySystem
 {
     [Dependency] private readonly IOverlayManager _overlayMan = default!;
 
-    public List<RadiationRay>? Rays;
+    public List<DebugRadiationRay>? Rays;
     public Dictionary<NetEntity, Dictionary<Vector2i, float>>? ResistanceGrids;
 
     public override void Initialize()
index a5f74e09e9db8c38d9b026fd2c43a548d9947bb6..44360905652a147d1934fd7b8c4fbd725b123bcf 100644 (file)
@@ -5,6 +5,7 @@ using Content.Shared.Administration;
 using Content.Shared.Radiation.Events;
 using Content.Shared.Radiation.Systems;
 using Robust.Shared.Console;
+using Robust.Shared.Debugging;
 using Robust.Shared.Enums;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
@@ -42,12 +43,12 @@ public partial class RadiationSystem
     /// </summary>
     private void UpdateDebugOverlay(EntityEventArgs ev)
     {
-        var sessions = _debugSessions.ToArray();
-        foreach (var session in sessions)
+        foreach (var session in _debugSessions)
         {
             if (session.Status != SessionStatus.InGame)
                 _debugSessions.Remove(session);
-            RaiseNetworkEvent(ev, session.Channel);
+            else
+                RaiseNetworkEvent(ev, session);
         }
     }
 
@@ -70,13 +71,16 @@ public partial class RadiationSystem
         UpdateDebugOverlay(ev);
     }
 
-    private void UpdateGridcastDebugOverlay(double elapsedTime, int totalSources,
-        int totalReceivers, List<RadiationRay> rays)
+    private void UpdateGridcastDebugOverlay(
+        double elapsedTime,
+        int totalSources,
+        int totalReceivers,
+        List<DebugRadiationRay>? rays)
     {
         if (_debugSessions.Count == 0)
             return;
 
-        var ev = new OnRadiationOverlayUpdateEvent(elapsedTime, totalSources, totalReceivers, rays);
+        var ev = new OnRadiationOverlayUpdateEvent(elapsedTime, totalSources, totalReceivers, rays ?? new());
         UpdateDebugOverlay(ev);
     }
 }
index ccee7cf227cbc15ba33fb669dfd048c6525c015f..15e1c352564b82e723b2718f25bda1999c2ff23f 100644 (file)
@@ -1,12 +1,9 @@
-using System.Linq;
 using System.Numerics;
 using Content.Server.Radiation.Components;
 using Content.Server.Radiation.Events;
 using Content.Shared.Radiation.Components;
 using Content.Shared.Radiation.Systems;
-using Content.Shared.Stacks;
 using Robust.Shared.Collections;
-using Robust.Shared.Containers;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
@@ -16,68 +13,86 @@ namespace Content.Server.Radiation.Systems;
 // main algorithm that fire radiation rays to target
 public partial class RadiationSystem
 {
-    [Dependency] private readonly SharedStackSystem _stack = default!;
-    [Dependency] private readonly SharedContainerSystem _container = default!;
+    private List<Entity<MapGridComponent>> _grids = new();
 
-    private EntityQuery<RadiationBlockingContainerComponent> _radiationBlockingContainers;
+    private readonly record struct SourceData(
+        float Intensity,
+        Entity<RadiationSourceComponent, TransformComponent> Entity,
+        Vector2 WorldPosition)
+    {
+        public EntityUid? GridUid => Entity.Comp2.GridUid;
+        public float Slope => Entity.Comp1.Slope;
+        public TransformComponent Transform => Entity.Comp2;
+    }
 
     private void UpdateGridcast()
     {
         // should we save debug information into rays?
         // if there is no debug sessions connected - just ignore it
-        var saveVisitedTiles = _debugSessions.Count > 0;
+        var debug = _debugSessions.Count > 0;
 
         var stopwatch = new Stopwatch();
         stopwatch.Start();
 
+        _sources.Clear();
+        _sources.EnsureCapacity(EntityManager.Count<RadiationSourceComponent>());
+
         var sources = EntityQueryEnumerator<RadiationSourceComponent, TransformComponent>();
         var destinations = EntityQueryEnumerator<RadiationReceiverComponent, TransformComponent>();
-        var resistanceQuery = GetEntityQuery<RadiationGridResistanceComponent>();
-        var transformQuery = GetEntityQuery<TransformComponent>();
-        var gridQuery = GetEntityQuery<MapGridComponent>();
-        var stackQuery = GetEntityQuery<StackComponent>();
-
-        _radiationBlockingContainers = GetEntityQuery<RadiationBlockingContainerComponent>();
 
-        // precalculate world positions for each source
-        // so we won't need to calc this in cycle over and over again
-        var sourcesData = new ValueList<(EntityUid, RadiationSourceComponent, TransformComponent, Vector2)>();
-        while (sources.MoveNext(out var uid, out var source, out var sourceTrs))
+        while (sources.MoveNext(out var uid, out var source, out var xform))
         {
             if (!source.Enabled)
                 continue;
 
-            var worldPos = _transform.GetWorldPosition(sourceTrs, transformQuery);
-            var data = (uid, source, sourceTrs, worldPos);
-            sourcesData.Add(data);
+            var worldPos = _transform.GetWorldPosition(xform);
+
+            // Intensity is scaled by stack size.
+            var intensity = source.Intensity * _stack.GetCount(uid);
+
+            // Apply rad modifier if the source is enclosed within a radiation blocking container
+            // Note that this also applies to receivers, and it doesn't bother to check if the container sits between them.
+            // I.e., a source & receiver in the same blocking container will get double-blocked, when no blocking should be applied.
+            intensity = GetAdjustedRadiationIntensity(uid, intensity);
+
+            _sources.Add(new(intensity, (uid, source, xform), worldPos));
         }
 
-        // trace all rays from rad source to rad receivers
-        var rays = new List<RadiationRay>();
+        var debugRays = debug ? new List<DebugRadiationRay>() : null;
         var receiversTotalRads = new ValueList<(Entity<RadiationReceiverComponent>, float)>();
+
+        // TODO RADIATION Parallelize
+        // Would need to give receiversTotalRads a fixed size.
+        // Also the _grids list needs to be local to a job. (or better yet cached in SourceData)
+        // And I guess disable parallelization when debugging to make populating the debug List<RadiationRay> easier.
+        // Or just make it threadsafe?
         while (destinations.MoveNext(out var destUid, out var dest, out var destTrs))
         {
-            var destWorld = _transform.GetWorldPosition(destTrs, transformQuery);
+            var destWorld = _transform.GetWorldPosition(destTrs);
 
             var rads = 0f;
-            foreach (var (uid, source, sourceTrs, sourceWorld) in sourcesData)
+            foreach (var source in _sources)
             {
-                stackQuery.TryGetComponent(uid, out var stack);
-                var intensity = source.Intensity * _stack.GetCount(uid, stack);
-
                 // send ray towards destination entity
-                var ray = Irradiate(uid, sourceTrs, sourceWorld,
-                    destUid, destTrs, destWorld,
-                    intensity, source.Slope, saveVisitedTiles, resistanceQuery, transformQuery, gridQuery);
-                if (ray == null)
+                if (Irradiate(source, destUid, destTrs, destWorld, debug) is not {} ray)
                     continue;
 
-                // save ray for debug
-                rays.Add(ray);
-
                 // add rads to total rad exposure
                 if (ray.ReachedDestination)
                     rads += ray.Rads;
+
+                if (!debug)
+                    continue;
+
+                debugRays!.Add(new DebugRadiationRay(
+                    ray.MapId,
+                    GetNetEntity(ray.SourceUid),
+                    ray.Source,
+                    GetNetEntity(ray.DestinationUid),
+                    ray.Destination,
+                    ray.Rads,
+                    ray.Blockers ?? new())
+                );
             }
 
             // Apply modifier if the destination entity is hidden within a radiation blocking container
@@ -88,9 +103,9 @@ public partial class RadiationSystem
 
         // update information for debug overlay
         var elapsedTime = stopwatch.Elapsed.TotalMilliseconds;
-        var totalSources = sourcesData.Count;
+        var totalSources = _sources.Count;
         var totalReceivers = receiversTotalRads.Count;
-        UpdateGridcastDebugOverlay(elapsedTime, totalSources, totalReceivers, rays);
+        UpdateGridcastDebugOverlay(elapsedTime, totalSources, totalReceivers, debugRays);
 
         // send rads to each entity
         foreach (var (receiver, rads) in receiversTotalRads)
@@ -108,19 +123,20 @@ public partial class RadiationSystem
         RaiseLocalEvent(new RadiationSystemUpdatedEvent());
     }
 
-    private RadiationRay? Irradiate(EntityUid sourceUid, TransformComponent sourceTrs, Vector2 sourceWorld,
-        EntityUid destUid, TransformComponent destTrs, Vector2 destWorld,
-        float incomingRads, float slope, bool saveVisitedTiles,
-        EntityQuery<RadiationGridResistanceComponent> resistanceQuery,
-        EntityQuery<TransformComponent> transformQuery, EntityQuery<MapGridComponent> gridQuery)
+    private RadiationRay? Irradiate(SourceData source,
+        EntityUid destUid,
+        TransformComponent destTrs,
+        Vector2 destWorld,
+        bool saveVisitedTiles)
     {
         // lets first check that source and destination on the same map
-        if (sourceTrs.MapID != destTrs.MapID)
+        if (source.Transform.MapID != destTrs.MapID)
             return null;
-        var mapId = sourceTrs.MapID;
+
+        var mapId = destTrs.MapID;
 
         // get direction from rad source to destination and its distance
-        var dir = destWorld - sourceWorld;
+        var dir = destWorld - source.WorldPosition;
         var dist = dir.Length();
 
         // check if receiver is too far away
@@ -128,41 +144,42 @@ public partial class RadiationSystem
             return null;
 
         // will it even reach destination considering distance penalty
-        var rads = incomingRads - slope * dist;
-
-        // Apply rad modifier if the source is enclosed within a radiation blocking container
-        rads = GetAdjustedRadiationIntensity(sourceUid, rads);
-
-        if (rads <= MinIntensity)
+        var rads = source.Intensity - source.Slope * dist;
+        if (rads < MinIntensity)
             return null;
 
         // create a new radiation ray from source to destination
         // at first we assume that it doesn't hit any radiation blockers
         // and has only distance penalty
-        var ray = new RadiationRay(mapId, GetNetEntity(sourceUid), sourceWorld, GetNetEntity(destUid), destWorld, rads);
+        var ray = new RadiationRay(mapId, source.Entity, source.WorldPosition, destUid, destWorld, rads);
 
         // if source and destination on the same grid it's possible that
         // between them can be another grid (ie. shuttle in center of donut station)
         // however we can do simplification and ignore that case
-        if (GridcastSimplifiedSameGrid && sourceTrs.GridUid != null && sourceTrs.GridUid == destTrs.GridUid)
+        if (GridcastSimplifiedSameGrid && destTrs.GridUid is {} gridUid && source.GridUid == gridUid)
         {
-            if (!gridQuery.TryGetComponent(sourceTrs.GridUid.Value, out var gridComponent))
+            if (!_gridQuery.TryGetComponent(gridUid, out var gridComponent))
                 return ray;
-            return Gridcast((sourceTrs.GridUid.Value, gridComponent), ray, saveVisitedTiles, resistanceQuery, sourceTrs, destTrs, transformQuery.GetComponent(sourceTrs.GridUid.Value));
+            return Gridcast((gridUid, gridComponent, Transform(gridUid)), ref ray, saveVisitedTiles, source.Transform, destTrs);
         }
 
         // lets check how many grids are between source and destination
         // do a box intersection test between target and destination
         // it's not very precise, but really cheap
-        var box = Box2.FromTwoPoints(sourceWorld, destWorld);
-        var grids = new List<Entity<MapGridComponent>>();
-        _mapManager.FindGridsIntersecting(mapId, box, ref grids, true);
+
+        // TODO RADIATION
+        // Consider caching this in SourceData?
+        // I.e., make the lookup for grids as large as the sources's max distance and store the result in SourceData.
+        // Avoids having to do a lookup per source*receiver.
+        var box = Box2.FromTwoPoints(source.WorldPosition, destWorld);
+        _grids.Clear();
+        _mapManager.FindGridsIntersecting(mapId, box, ref _grids, true);
 
         // gridcast through each grid and try to hit some radiation blockers
         // the ray will be updated with each grid that has some blockers
-        foreach (var grid in grids)
+        foreach (var grid in _grids)
         {
-            ray = Gridcast(grid, ray, saveVisitedTiles, resistanceQuery, sourceTrs, destTrs, transformQuery.GetComponent(grid));
+            ray = Gridcast((grid.Owner, grid.Comp, Transform(grid)), ref ray, saveVisitedTiles, source.Transform, destTrs);
 
             // looks like last grid blocked all radiation
             // we can return right now
@@ -170,20 +187,23 @@ public partial class RadiationSystem
                 return ray;
         }
 
+        _grids.Clear();
+
         return ray;
     }
 
-    private RadiationRay Gridcast(Entity<MapGridComponent> grid, RadiationRay ray, bool saveVisitedTiles,
-        EntityQuery<RadiationGridResistanceComponent> resistanceQuery,
+    private RadiationRay Gridcast(
+        Entity<MapGridComponent, TransformComponent> grid,
+        ref RadiationRay ray,
+        bool saveVisitedTiles,
         TransformComponent sourceTrs,
-        TransformComponent destTrs,
-        TransformComponent gridTrs)
+        TransformComponent destTrs)
     {
-        var blockers = new List<(Vector2i, float)>();
+        var blockers = saveVisitedTiles ? new List<(Vector2i, float)>() : null;
 
         // if grid doesn't have resistance map just apply distance penalty
         var gridUid = grid.Owner;
-        if (!resistanceQuery.TryGetComponent(gridUid, out var resistance))
+        if (!_resistanceQuery.TryGetComponent(gridUid, out var resistance))
             return ray;
         var resistanceMap = resistance.ResistancePerTile;
 
@@ -195,19 +215,19 @@ public partial class RadiationSystem
 
         Vector2 srcLocal = sourceTrs.ParentUid == grid.Owner
             ? sourceTrs.LocalPosition
-            : Vector2.Transform(ray.Source, gridTrs.InvLocalMatrix);
+            : Vector2.Transform(ray.Source, grid.Comp2.InvLocalMatrix);
 
         Vector2 dstLocal = destTrs.ParentUid == grid.Owner
             ? destTrs.LocalPosition
-            : Vector2.Transform(ray.Destination, gridTrs.InvLocalMatrix);
+            : Vector2.Transform(ray.Destination, grid.Comp2.InvLocalMatrix);
 
         Vector2i sourceGrid = new(
-            (int) Math.Floor(srcLocal.X / grid.Comp.TileSize),
-            (int) Math.Floor(srcLocal.Y / grid.Comp.TileSize));
+            (int) Math.Floor(srcLocal.X / grid.Comp1.TileSize),
+            (int) Math.Floor(srcLocal.Y / grid.Comp1.TileSize));
 
         Vector2i destGrid = new(
-            (int) Math.Floor(dstLocal.X / grid.Comp.TileSize),
-            (int) Math.Floor(dstLocal.Y / grid.Comp.TileSize));
+            (int) Math.Floor(dstLocal.X / grid.Comp1.TileSize),
+            (int) Math.Floor(dstLocal.Y / grid.Comp1.TileSize));
 
         // iterate tiles in grid line from source to destination
         var line = new GridLineEnumerator(sourceGrid, destGrid);
@@ -220,7 +240,7 @@ public partial class RadiationSystem
 
             // save data for debug
             if (saveVisitedTiles)
-                blockers.Add((point, ray.Rads));
+                blockers!.Add((point, ray.Rads));
 
             // no intensity left after blocker
             if (ray.Rads <= MinIntensity)
@@ -230,21 +250,45 @@ public partial class RadiationSystem
             }
         }
 
+        if (!saveVisitedTiles || blockers!.Count <= 0)
+            return ray;
+
         // save data for debug if needed
-        if (saveVisitedTiles && blockers.Count > 0)
-            ray.Blockers.Add(GetNetEntity(gridUid), blockers);
+        ray.Blockers ??= new();
+        ray.Blockers.Add(GetNetEntity(gridUid), blockers);
 
         return ray;
     }
 
     private float GetAdjustedRadiationIntensity(EntityUid uid, float rads)
     {
-        var radblockingComps = new List<RadiationBlockingContainerComponent>();
-        if (_container.TryFindComponentsOnEntityContainerOrParent(uid, _radiationBlockingContainers, radblockingComps))
+        var child = uid;
+        var xform = Transform(uid);
+        var parent = xform.ParentUid;
+
+        while (parent.IsValid())
         {
-            rads -= radblockingComps.Sum(x => x.RadResistance);
+            var parentXform = Transform(parent);
+            var childMeta = MetaData(child);
+
+            if ((childMeta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer)
+            {
+                child = parent;
+                parent = parentXform.ParentUid;
+                continue;
+            }
+
+            if (_blockerQuery.TryComp(xform.ParentUid, out var blocker))
+            {
+                rads -= blocker.RadResistance;
+                if (rads < 0)
+                    return 0;
+            }
+
+            child = parent;
+            parent = parentXform.ParentUid;
         }
 
-        return float.Max(rads, 0);
+        return rads;
     }
 }
index e184c022f2f4c441c8bb703e12fca0bdf78301f6..3ba393959c2568f832ea1f127621adda0bbd4201 100644 (file)
@@ -1,8 +1,10 @@
 using Content.Server.Radiation.Components;
 using Content.Shared.Radiation.Components;
 using Content.Shared.Radiation.Events;
+using Content.Shared.Stacks;
 using Robust.Shared.Configuration;
 using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
 
 namespace Content.Server.Radiation.Systems;
 
@@ -11,14 +13,26 @@ public sealed partial class RadiationSystem : EntitySystem
     [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly SharedStackSystem _stack = default!;
+
+    private EntityQuery<RadiationBlockingContainerComponent> _blockerQuery;
+    private EntityQuery<RadiationGridResistanceComponent> _resistanceQuery;
+    private EntityQuery<MapGridComponent> _gridQuery;
+    private EntityQuery<StackComponent> _stackQuery;
 
     private float _accumulator;
+    private List<SourceData> _sources = new();
 
     public override void Initialize()
     {
         base.Initialize();
         SubscribeCvars();
         InitRadBlocking();
+
+        _blockerQuery = GetEntityQuery<RadiationBlockingContainerComponent>();
+        _resistanceQuery = GetEntityQuery<RadiationGridResistanceComponent>();
+        _gridQuery = GetEntityQuery<MapGridComponent>();
+        _stackQuery = GetEntityQuery<StackComponent>();
     }
 
     public override void Update(float frameTime)
index ee35304e2af7547c4e3c09fc9a33b164043a3a06..3112e1eb471ecdf9da612045509fc909ccbfd34a 100644 (file)
@@ -4,17 +4,11 @@
 ///     Raised on entity when it was irradiated
 ///     by some radiation source.
 /// </summary>
-public sealed class OnIrradiatedEvent : EntityEventArgs
+public readonly record struct OnIrradiatedEvent(float FrameTime, float RadsPerSecond)
 {
-    public readonly float FrameTime;
+    public readonly float FrameTime = FrameTime;
 
-    public readonly float RadsPerSecond;
+    public readonly float RadsPerSecond = RadsPerSecond;
 
     public float TotalRads => RadsPerSecond * FrameTime;
-
-    public OnIrradiatedEvent(float frameTime, float radsPerSecond)
-    {
-        FrameTime = frameTime;
-        RadsPerSecond = radsPerSecond;
-    }
 }
index a93ca4c616b02380443da39898df0c59530ccaa7..e42d13ffb7b28b5e8d75c09dc0bb8043c863c39a 100644 (file)
@@ -13,36 +13,33 @@ namespace Content.Shared.Radiation.Events;
 ///     Will be sent only to clients that activated radiation view using console command.
 /// </remarks>
 [Serializable, NetSerializable]
-public sealed class OnRadiationOverlayUpdateEvent : EntityEventArgs
+public sealed class OnRadiationOverlayUpdateEvent(
+    double elapsedTimeMs,
+    int sourcesCount,
+    int receiversCount,
+    List<DebugRadiationRay> rays)
+    : EntityEventArgs
 {
     /// <summary>
     ///     Total time in milliseconds that server took to do radiation processing.
     ///     Exclude time of entities reacting to <see cref="OnIrradiatedEvent"/>.
     /// </summary>
-    public readonly double ElapsedTimeMs;
+    public readonly double ElapsedTimeMs = elapsedTimeMs;
 
     /// <summary>
     ///     Total count of entities with <see cref="RadiationSourceComponent"/> on all maps.
     /// </summary>
-    public readonly int SourcesCount;
+    public readonly int SourcesCount = sourcesCount;
 
     /// <summary>
     ///     Total count of entities with radiation receiver on all maps.
     /// </summary>
-    public readonly int ReceiversCount;
+    public readonly int ReceiversCount = receiversCount;
 
     /// <summary>
     ///     All radiation rays that was processed by radiation system.
     /// </summary>
-    public readonly List<RadiationRay> Rays;
-
-    public OnRadiationOverlayUpdateEvent(double elapsedTimeMs, int sourcesCount, int receiversCount, List<RadiationRay> rays)
-    {
-        ElapsedTimeMs = elapsedTimeMs;
-        SourcesCount = sourcesCount;
-        ReceiversCount = receiversCount;
-        Rays = rays;
-    }
+    public readonly List<DebugRadiationRay> Rays = rays;
 }
 
 /// <summary>
index ca8ab5af661dc9785e59ed97b9a7cd8485cdf2d7..9f8b9596943aaf506b71bd18472a31b02b4c68b8 100644 (file)
@@ -9,33 +9,38 @@ namespace Content.Shared.Radiation.Systems;
 ///     Ray emitted by radiation source towards radiation receiver.
 ///     Contains all information about encountered radiation blockers.
 /// </summary>
-[Serializable, NetSerializable]
-public sealed class RadiationRay
+public struct RadiationRay(
+    MapId mapId,
+    EntityUid sourceUid,
+    Vector2 source,
+    EntityUid destinationUid,
+    Vector2 destination,
+    float rads)
 {
     /// <summary>
     ///     Map on which source and receiver are placed.
     /// </summary>
-    public MapId MapId;
+    public MapId MapId = mapId;
     /// <summary>
     ///     Uid of entity with <see cref="RadiationSourceComponent"/>.
     /// </summary>
-    public NetEntity SourceUid;
+    public EntityUid SourceUid = sourceUid;
     /// <summary>
     ///     World coordinates of radiation source.
     /// </summary>
-    public Vector2 Source;
+    public Vector2 Source = source;
     /// <summary>
     ///     Uid of entity with radiation receiver component.
     /// </summary>
-    public NetEntity DestinationUid;
+    public EntityUid DestinationUid = destinationUid;
     /// <summary>
     ///     World coordinates of radiation receiver.
     /// </summary>
-    public Vector2 Destination;
+    public Vector2 Destination = destination;
     /// <summary>
     ///     How many rads intensity reached radiation receiver.
     /// </summary>
-    public float Rads;
+    public float Rads = rads;
 
     /// <summary>
     ///     Has rad ray reached destination or lost all intensity after blockers?
@@ -43,23 +48,27 @@ public sealed class RadiationRay
     public bool ReachedDestination => Rads > 0;
 
     /// <summary>
-    ///     All blockers visited by gridcast. Key is uid of grid. Values are pairs
+    ///     All blockers visited by gridcast, used for debug overlays. Key is uid of grid. Values are pairs
     ///     of tile indices and floats with updated radiation value.
     /// </summary>
     /// <remarks>
     ///     Last tile may have negative value if ray has lost all intensity.
     ///     Grid traversal order isn't guaranteed.
     /// </remarks>
-    public Dictionary<NetEntity, List<(Vector2i, float)>> Blockers = new();
+    public Dictionary<NetEntity, List<(Vector2i, float)>>? Blockers;
+
+}
 
-    public RadiationRay(MapId mapId, NetEntity sourceUid, Vector2 source,
-        NetEntity destinationUid, Vector2 destination, float rads)
-    {
-        MapId = mapId;
-        SourceUid = sourceUid;
-        Source = source;
-        DestinationUid = destinationUid;
-        Destination = destination;
-        Rads = rads;
-    }
+// Variant of RadiationRay that uses NetEntities.
+[Serializable, NetSerializable]
+public readonly record struct DebugRadiationRay(
+    MapId MapId,
+    NetEntity SourceUid,
+    Vector2 Source,
+    NetEntity DestinationUid,
+    Vector2 Destination,
+    float Rads,
+    Dictionary<NetEntity, List<(Vector2i, float)>> Blockers)
+{
+    public bool ReachedDestination => Rads > 0;
 }