Optimize & clean up RadiationSystem (#34459)

* 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>
This commit is contained in:
Leon Friedrich
2025-01-18 12:07:20 +11:00
committed by GitHub
parent efd5d644e8
commit 9d4e60068b
7 changed files with 189 additions and 127 deletions

View 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()

View 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);
}
}

View 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;
}
}

View 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)

View 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;
}
}

View 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>

View 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;
}