diff --git a/Content.Client/Radiation/Systems/RadiationSystem.cs b/Content.Client/Radiation/Systems/RadiationSystem.cs index 929ad6aa4ac58e..f4f109adc7c363 100644 --- a/Content.Client/Radiation/Systems/RadiationSystem.cs +++ b/Content.Client/Radiation/Systems/RadiationSystem.cs @@ -9,7 +9,7 @@ public sealed class RadiationSystem : EntitySystem { [Dependency] private readonly IOverlayManager _overlayMan = default!; - public List? Rays; + public List? Rays; public Dictionary>? ResistanceGrids; public override void Initialize() diff --git a/Content.Server/Radiation/Systems/RadiationSystem.Debug.cs b/Content.Server/Radiation/Systems/RadiationSystem.Debug.cs index a5f74e09e9db8c..44360905652a14 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.Debug.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.Debug.cs @@ -5,6 +5,7 @@ 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 void ToggleDebugView(ICommonSession session) /// 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 @@ private void UpdateResistanceDebugOverlay() UpdateDebugOverlay(ev); } - private void UpdateGridcastDebugOverlay(double elapsedTime, int totalSources, - int totalReceivers, List rays) + private void UpdateGridcastDebugOverlay( + double elapsedTime, + int totalSources, + int totalReceivers, + List? 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); } } diff --git a/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs b/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs index ccee7cf227cbc1..15e1c352564b82 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs @@ -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> _grids = new(); - private EntityQuery _radiationBlockingContainers; + private readonly record struct SourceData( + float Intensity, + Entity 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()); + var sources = EntityQueryEnumerator(); var destinations = EntityQueryEnumerator(); - var resistanceQuery = GetEntityQuery(); - var transformQuery = GetEntityQuery(); - var gridQuery = GetEntityQuery(); - var stackQuery = GetEntityQuery(); - - _radiationBlockingContainers = GetEntityQuery(); - // 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(); + var debugRays = debug ? new List() : null; var receiversTotalRads = new ValueList<(Entity, 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 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 @@ private void UpdateGridcast() // 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 @@ private void UpdateGridcast() 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 resistanceQuery, - EntityQuery transformQuery, EntityQuery 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 @@ private void UpdateGridcast() 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>(); - _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 @@ private void UpdateGridcast() return ray; } + _grids.Clear(); + return ray; } - private RadiationRay Gridcast(Entity grid, RadiationRay ray, bool saveVisitedTiles, - EntityQuery resistanceQuery, + private RadiationRay Gridcast( + Entity 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 @@ private RadiationRay Gridcast(Entity grid, RadiationRay ray, b 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 @@ private RadiationRay Gridcast(Entity grid, RadiationRay ray, b // 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 @@ private RadiationRay Gridcast(Entity grid, RadiationRay ray, b } } + 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(); - 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; } } diff --git a/Content.Server/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs index e184c022f2f4c4..3ba393959c2568 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.cs @@ -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 _blockerQuery; + private EntityQuery _resistanceQuery; + private EntityQuery _gridQuery; + private EntityQuery _stackQuery; private float _accumulator; + private List _sources = new(); public override void Initialize() { base.Initialize(); SubscribeCvars(); InitRadBlocking(); + + _blockerQuery = GetEntityQuery(); + _resistanceQuery = GetEntityQuery(); + _gridQuery = GetEntityQuery(); + _stackQuery = GetEntityQuery(); } public override void Update(float frameTime) diff --git a/Content.Shared/Radiation/Events/OnIrradiatedEvent.cs b/Content.Shared/Radiation/Events/OnIrradiatedEvent.cs index ee35304e2af754..3112e1eb471ecd 100644 --- a/Content.Shared/Radiation/Events/OnIrradiatedEvent.cs +++ b/Content.Shared/Radiation/Events/OnIrradiatedEvent.cs @@ -4,17 +4,11 @@ /// Raised on entity when it was irradiated /// by some radiation source. /// -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; - } } diff --git a/Content.Shared/Radiation/Events/OnRadiationOverlayUpdateEvent.cs b/Content.Shared/Radiation/Events/OnRadiationOverlayUpdateEvent.cs index a93ca4c616b023..e42d13ffb7b28b 100644 --- a/Content.Shared/Radiation/Events/OnRadiationOverlayUpdateEvent.cs +++ b/Content.Shared/Radiation/Events/OnRadiationOverlayUpdateEvent.cs @@ -13,36 +13,33 @@ namespace Content.Shared.Radiation.Events; /// Will be sent only to clients that activated radiation view using console command. /// [Serializable, NetSerializable] -public sealed class OnRadiationOverlayUpdateEvent : EntityEventArgs +public sealed class OnRadiationOverlayUpdateEvent( + double elapsedTimeMs, + int sourcesCount, + int receiversCount, + List rays) + : EntityEventArgs { /// /// Total time in milliseconds that server took to do radiation processing. /// Exclude time of entities reacting to . /// - public readonly double ElapsedTimeMs; + public readonly double ElapsedTimeMs = elapsedTimeMs; /// /// Total count of entities with on all maps. /// - public readonly int SourcesCount; + public readonly int SourcesCount = sourcesCount; /// /// Total count of entities with radiation receiver on all maps. /// - public readonly int ReceiversCount; + public readonly int ReceiversCount = receiversCount; /// /// All radiation rays that was processed by radiation system. /// - public readonly List Rays; - - public OnRadiationOverlayUpdateEvent(double elapsedTimeMs, int sourcesCount, int receiversCount, List rays) - { - ElapsedTimeMs = elapsedTimeMs; - SourcesCount = sourcesCount; - ReceiversCount = receiversCount; - Rays = rays; - } + public readonly List Rays = rays; } /// diff --git a/Content.Shared/Radiation/RadiationRay.cs b/Content.Shared/Radiation/RadiationRay.cs index ca8ab5af661dc9..9f8b9596943aaf 100644 --- a/Content.Shared/Radiation/RadiationRay.cs +++ b/Content.Shared/Radiation/RadiationRay.cs @@ -9,33 +9,38 @@ namespace Content.Shared.Radiation.Systems; /// Ray emitted by radiation source towards radiation receiver. /// Contains all information about encountered radiation blockers. /// -[Serializable, NetSerializable] -public sealed class RadiationRay +public struct RadiationRay( + MapId mapId, + EntityUid sourceUid, + Vector2 source, + EntityUid destinationUid, + Vector2 destination, + float rads) { /// /// Map on which source and receiver are placed. /// - public MapId MapId; + public MapId MapId = mapId; /// /// Uid of entity with . /// - public NetEntity SourceUid; + public EntityUid SourceUid = sourceUid; /// /// World coordinates of radiation source. /// - public Vector2 Source; + public Vector2 Source = source; /// /// Uid of entity with radiation receiver component. /// - public NetEntity DestinationUid; + public EntityUid DestinationUid = destinationUid; /// /// World coordinates of radiation receiver. /// - public Vector2 Destination; + public Vector2 Destination = destination; /// /// How many rads intensity reached radiation receiver. /// - public float Rads; + public float Rads = rads; /// /// Has rad ray reached destination or lost all intensity after blockers? @@ -43,23 +48,27 @@ public sealed class RadiationRay public bool ReachedDestination => Rads > 0; /// - /// 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. /// /// /// Last tile may have negative value if ray has lost all intensity. /// Grid traversal order isn't guaranteed. /// - public Dictionary> Blockers = new(); + public Dictionary>? 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> Blockers) +{ + public bool ReachedDestination => Rads > 0; }