Radiation rework (#10970)

This commit is contained in:
Alex Evgrashin
2022-10-11 05:09:10 +02:00
committed by GitHub
parent 667fc1970d
commit 7d882f22c9
34 changed files with 1010 additions and 46 deletions

View File

@@ -15,7 +15,7 @@ using Content.Client.MainMenu;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences; using Content.Client.Preferences;
using Content.Client.Radiation; using Content.Client.Radiation.Overlays;
using Content.Client.Screenshot; using Content.Client.Screenshot;
using Content.Client.Singularity; using Content.Client.Singularity;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;

View File

@@ -0,0 +1,131 @@
using System.Linq;
using Content.Client.Radiation.Systems;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Map;
namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly RadiationSystem _radiation;
private readonly Font _font;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
public RadiationDebugOverlay()
{
IoCManager.InjectDependencies(this);
_radiation = _entityManager.System<RadiationSystem>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)
{
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreenRays(args);
DrawScreenResistance(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawScreenRays(OverlayDrawArgs args)
{
var rays = _radiation.Rays;
if (rays == null || args.ViewportControl == null)
return;
var handle = args.ScreenHandle;
foreach (var ray in rays)
{
if (ray.MapId != args.MapId)
continue;
if (ray.ReachedDestination)
{
var screenCenter = args.ViewportControl.WorldToScreen(ray.Destination);
handle.DrawString(_font, screenCenter, ray.Rads.ToString("F2"), 2f, Color.White);
}
foreach (var (gridUid, blockers) in ray.Blockers)
{
if (!_mapManager.TryGetGrid(gridUid, out var grid))
continue;
foreach (var (tile, rads) in blockers)
{
var worldPos = grid.GridTileToWorldPos(tile);
var screenCenter = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenCenter, rads.ToString("F2"), 1.5f, Color.White);
}
}
}
}
private void DrawScreenResistance(OverlayDrawArgs args)
{
var resistance = _radiation.ResistanceGrids;
if (resistance == null || args.ViewportControl == null)
return;
var handle = args.ScreenHandle;
var query = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var (gridUid, resMap) in resistance)
{
if (!_mapManager.TryGetGrid(gridUid, out var grid))
continue;
if (query.TryGetComponent(gridUid, out var trs) && trs.MapID != args.MapId)
continue;
var offset = new Vector2(grid.TileSize, -grid.TileSize) * 0.25f;
foreach (var (tile, value) in resMap)
{
var localPos = grid.GridTileToLocal(tile).Position + offset;
var worldPos = grid.LocalToWorld(localPos);
var screenCenter = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenCenter, value.ToString("F2"), color: Color.White);
}
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
var rays = _radiation.Rays;
if (rays == null)
return;
var handle = args.WorldHandle;
// draw lines for raycasts
foreach (var ray in rays)
{
if (ray.MapId != args.MapId)
continue;
if (ray.ReachedDestination)
{
handle.DrawLine(ray.Source, ray.Destination, Color.Red);
continue;
}
foreach (var (gridUid, blockers) in ray.Blockers)
{
if (!_mapManager.TryGetGrid(gridUid, out var grid))
continue;
var (destTile, _) = blockers.Last();
var destWorld = grid.GridTileToWorldPos(destTile);
handle.DrawLine(ray.Source, destWorld, Color.Red);
}
}
}
}

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.Radiation namespace Content.Client.Radiation.Overlays
{ {
public sealed class RadiationPulseOverlay : Overlay public sealed class RadiationPulseOverlay : Overlay
{ {

View File

@@ -0,0 +1,54 @@
using Content.Client.Radiation.Overlays;
using Content.Shared.Radiation.Events;
using Content.Shared.Radiation.Systems;
using Robust.Client.Graphics;
namespace Content.Client.Radiation.Systems;
public sealed class RadiationSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
public List<RadiationRay>? Rays;
public Dictionary<EntityUid, Dictionary<Vector2i, float>>? ResistanceGrids;
public override void Initialize()
{
SubscribeNetworkEvent<OnRadiationOverlayToggledEvent>(OnOverlayToggled);
SubscribeNetworkEvent<OnRadiationOverlayUpdateEvent>(OnOverlayUpdate);
SubscribeNetworkEvent<OnRadiationOverlayResistanceUpdateEvent>(OnResistanceUpdate);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<RadiationDebugOverlay>();
}
private void OnOverlayToggled(OnRadiationOverlayToggledEvent ev)
{
if (ev.IsEnabled)
_overlayMan.AddOverlay(new RadiationDebugOverlay());
else
_overlayMan.RemoveOverlay<RadiationDebugOverlay>();
}
private void OnOverlayUpdate(OnRadiationOverlayUpdateEvent ev)
{
if (!_overlayMan.TryGetOverlay(out RadiationDebugOverlay? overlay))
return;
var str = $"Radiation update: {ev.ElapsedTimeMs}ms with. Receivers: {ev.ReceiversCount}, " +
$"Sources: {ev.SourcesCount}, Rays: {ev.Rays.Count}";
Logger.Info(str);
Rays = ev.Rays;
}
private void OnResistanceUpdate(OnRadiationOverlayResistanceUpdateEvent ev)
{
if (!_overlayMan.TryGetOverlay(out RadiationDebugOverlay? overlay))
return;
ResistanceGrids = ev.Grids;
}
}

View File

@@ -0,0 +1,29 @@
using Content.Server.Radiation.Systems;
namespace Content.Server.Radiation.Components;
/// <summary>
/// Blocks radiation when placed on tile.
/// </summary>
[RegisterComponent]
[Access(typeof(RadiationSystem))]
public sealed class RadiationBlockerComponent : Component
{
/// <summary>
/// Does it block radiation at all?
/// </summary>
[DataField("enabled")]
public bool Enabled = true;
/// <summary>
/// How many rads per second does the blocker absorb?
/// </summary>
[DataField("resistance")]
public float RadResistance = 1f;
/// <summary>
/// Current position of the rad blocker in grid coordinates.
/// Null if doesn't anchored or doesn't block rads.
/// </summary>
public (EntityUid Grid, Vector2i Tile)? CurrentPosition;
}

View File

@@ -0,0 +1,16 @@
using Content.Server.Radiation.Systems;
namespace Content.Server.Radiation.Components;
/// <summary>
/// Grid component that stores radiation resistance of <see cref="RadiationBlockerComponent"/> per tile.
/// </summary>
[RegisterComponent]
[Access(typeof(RadiationSystem), Other = AccessPermissions.ReadExecute)]
public sealed class RadiationGridResistanceComponent : Component
{
/// <summary>
/// Radiation resistance per tile.
/// </summary>
public readonly Dictionary<Vector2i, float> ResistancePerTile = new();
}

View File

@@ -0,0 +1,20 @@
using Content.Server.Radiation.Systems;
using Content.Shared.Radiation.Components;
namespace Content.Server.Radiation.Components;
/// <summary>
/// Marks component that receive radiation from <see cref="RadiationSourceComponent"/>.
/// </summary>
[RegisterComponent]
[Access(typeof(RadiationSystem))]
public sealed class RadiationReceiverComponent : Component
{
/// <summary>
/// Current radiation value in rads per second.
/// Periodically updated by radiation system.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public float CurrentRadiation;
}

View File

@@ -0,0 +1,166 @@
using Content.Server.Radiation.Components;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
namespace Content.Server.Radiation.Systems;
// create and update map of radiation blockers
public partial class RadiationSystem
{
private void InitRadBlocking()
{
SubscribeLocalEvent<RadiationBlockerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<RadiationBlockerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RadiationBlockerComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<RadiationBlockerComponent, ReAnchorEvent>(OnReAnchor);
SubscribeLocalEvent<RadiationBlockerComponent, DoorStateChangedEvent>(OnDoorChanged);
SubscribeLocalEvent<RadiationGridResistanceComponent, EntityTerminatingEvent>(OnGridRemoved);
}
private void OnInit(EntityUid uid, RadiationBlockerComponent component, ComponentInit args)
{
if (!component.Enabled)
return;
AddTile(uid, component);
}
private void OnShutdown(EntityUid uid, RadiationBlockerComponent component, ComponentShutdown args)
{
if (component.Enabled)
return;
RemoveTile(uid, component);
}
private void OnAnchorChanged(EntityUid uid, RadiationBlockerComponent component, ref AnchorStateChangedEvent args)
{
if (args.Anchored)
{
AddTile(uid, component);
}
else
{
RemoveTile(uid, component);
}
}
private void OnReAnchor(EntityUid uid, RadiationBlockerComponent component, ref ReAnchorEvent args)
{
// probably grid was split
// we need to remove entity from old resistance map
RemoveTile(uid, component);
// and move it to the new one
AddTile(uid, component);
}
private void OnDoorChanged(EntityUid uid, RadiationBlockerComponent component, DoorStateChangedEvent args)
{
switch (args.State)
{
case DoorState.Open:
SetEnabled(uid, false, component);
break;
case DoorState.Closed:
SetEnabled(uid, true, component);
break;
}
}
private void OnGridRemoved(EntityUid uid, RadiationGridResistanceComponent component, ref EntityTerminatingEvent args)
{
// grid is about to be removed - lets delete grid component first
// this should save a bit performance when blockers will be deleted
RemComp(uid, component);
}
public void SetEnabled(EntityUid uid, bool isEnabled, RadiationBlockerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (isEnabled == component.Enabled)
return;
component.Enabled = isEnabled;
if (!component.Enabled)
RemoveTile(uid, component);
else
AddTile(uid, component);
}
private void AddTile(EntityUid uid, RadiationBlockerComponent component)
{
// check that last position was removed
if (component.CurrentPosition != null)
{
RemoveTile(uid, component);
}
// check if entity even provide some rad protection
if (!component.Enabled || component.RadResistance <= 0)
return;
// check if it's on a grid
var trs = Transform(uid);
if (!trs.Anchored || !TryComp(trs.GridUid, out IMapGridComponent? grid))
return;
// save resistance into rad protection grid
var gridId = trs.GridUid.Value;
var tilePos = grid.Grid.TileIndicesFor(trs.Coordinates);
AddToTile(gridId, tilePos, component.RadResistance);
// and remember it as last valid position
component.CurrentPosition = (gridId, tilePos);
}
private void RemoveTile(EntityUid uid, RadiationBlockerComponent component)
{
// check if blocker was placed on grid before component was removed
if (component.CurrentPosition == null)
return;
var (gridId, tilePos) = component.CurrentPosition.Value;
// try to remove
RemoveFromTile(gridId, tilePos, component.RadResistance);
component.CurrentPosition = null;
}
private void AddToTile(EntityUid gridUid, Vector2i tilePos, float radResistance)
{
// get existing rad resistance grid or create it if it doesn't exist
var resistance = EnsureComp<RadiationGridResistanceComponent>(gridUid);
var grid = resistance.ResistancePerTile;
// add to existing cell more rad resistance
var newResistance = radResistance;
if (grid.TryGetValue(tilePos, out var existingResistance))
{
newResistance += existingResistance;
}
grid[tilePos] = newResistance;
}
private void RemoveFromTile(EntityUid gridUid, Vector2i tilePos, float radResistance)
{
// get grid
if (!TryComp(gridUid, out RadiationGridResistanceComponent? resistance))
return;
var grid = resistance.ResistancePerTile;
// subtract resistance from tile
if (!grid.TryGetValue(tilePos, out var existingResistance))
return;
existingResistance -= radResistance;
// remove tile from grid if no resistance left
if (existingResistance > 0)
grid[tilePos] = existingResistance;
else
{
grid.Remove(tilePos);
if (grid.Count == 0)
RemComp(gridUid, resistance);
}
}
}

View File

@@ -0,0 +1,48 @@
using Content.Shared.CCVar;
namespace Content.Server.Radiation.Systems;
// cvar updates
public partial class RadiationSystem
{
public float MinIntensity { get; private set; }
public float GridcastUpdateRate { get; private set; }
public bool GridcastSimplifiedSameGrid { get; private set; }
public float GridcastMaxDistance { get; private set; }
private void SubscribeCvars()
{
_cfg.OnValueChanged(CCVars.RadiationMinIntensity, SetMinRadiationIntensity, true);
_cfg.OnValueChanged(CCVars.RadiationGridcastUpdateRate, SetGridcastUpdateRate, true);
_cfg.OnValueChanged(CCVars.RadiationGridcastSimplifiedSameGrid, SetGridcastSimplifiedSameGrid, true);
_cfg.OnValueChanged(CCVars.RadiationGridcastMaxDistance, SetGridcastMaxDistance, true);
}
private void UnsubscribeCvars()
{
_cfg.UnsubValueChanged(CCVars.RadiationMinIntensity, SetMinRadiationIntensity);
_cfg.UnsubValueChanged(CCVars.RadiationGridcastUpdateRate, SetGridcastUpdateRate);
_cfg.UnsubValueChanged(CCVars.RadiationGridcastSimplifiedSameGrid, SetGridcastSimplifiedSameGrid);
_cfg.UnsubValueChanged(CCVars.RadiationGridcastMaxDistance, SetGridcastMaxDistance);
}
private void SetMinRadiationIntensity(float radiationMinIntensity)
{
MinIntensity = radiationMinIntensity;
}
private void SetGridcastUpdateRate(float updateRate)
{
GridcastUpdateRate = updateRate;
}
private void SetGridcastSimplifiedSameGrid(bool simplifiedSameGrid)
{
GridcastSimplifiedSameGrid = simplifiedSameGrid;
}
private void SetGridcastMaxDistance(float maxDistance)
{
GridcastMaxDistance = maxDistance;
}
}

View File

@@ -0,0 +1,105 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Radiation.Components;
using Content.Shared.Administration;
using Content.Shared.Radiation.Events;
using Content.Shared.Radiation.Systems;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Players;
namespace Content.Server.Radiation.Systems;
// radiation overlay debug logic
// rad rays send only to clients that enabled debug overlay
public partial class RadiationSystem
{
private readonly HashSet<ICommonSession> _debugSessions = new();
/// <summary>
/// Toggle radiation debug overlay for selected player.
/// </summary>
public void ToggleDebugView(ICommonSession session)
{
bool isEnabled;
if (_debugSessions.Add(session))
{
isEnabled = true;
}
else
{
_debugSessions.Remove(session);
isEnabled = false;
}
var ev = new OnRadiationOverlayToggledEvent(isEnabled);
RaiseNetworkEvent(ev, session.ConnectedClient);
}
/// <summary>
/// Send new information for radiation overlay.
/// </summary>
private void UpdateDebugOverlay(EntityEventArgs ev)
{
var sessions = _debugSessions.ToArray();
foreach (var session in sessions)
{
if (session.Status != SessionStatus.InGame)
_debugSessions.Remove(session);
RaiseNetworkEvent(ev, session.ConnectedClient);
}
}
private void UpdateResistanceDebugOverlay()
{
if (_debugSessions.Count == 0)
return;
var query = GetEntityQuery<RadiationGridResistanceComponent>();
var dict = new Dictionary<EntityUid, Dictionary<Vector2i, float>>();
foreach (var grid in _mapManager.GetAllGrids())
{
var gridUid = grid.GridEntityId;
if (!query.TryGetComponent(gridUid, out var resistance))
continue;
var resMap = resistance.ResistancePerTile;
dict.Add(gridUid, resMap);
}
var ev = new OnRadiationOverlayResistanceUpdateEvent(dict);
UpdateDebugOverlay(ev);
}
private void UpdateGridcastDebugOverlay(double elapsedTime, int totalSources,
int totalReceivers, List<RadiationRay> rays)
{
if (_debugSessions.Count == 0)
return;
var ev = new OnRadiationOverlayUpdateEvent(elapsedTime, totalSources, totalReceivers, rays);
UpdateDebugOverlay(ev);
}
}
/// <summary>
/// Toggle visibility of radiation rays coming from rad sources.
/// </summary>
[AdminCommand(AdminFlags.Admin)]
public sealed class RadiationViewCommand : IConsoleCommand
{
public string Command => "showradiation";
public string Description => Loc.GetString("radiation-command-description");
public string Help => Loc.GetString("radiation-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var session = shell.Player;
if (session == null)
return;
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.System<RadiationSystem>().ToggleDebugView(session);
}
}

View File

@@ -0,0 +1,186 @@
using Content.Server.Radiation.Components;
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Radiation.Systems;
// main algorithm that fire radiation rays to target
public partial class RadiationSystem
{
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 stopwatch = new Stopwatch();
stopwatch.Start();
var sources = EntityQuery<RadiationSourceComponent, TransformComponent>();
var destinations = EntityQuery<RadiationReceiverComponent, TransformComponent>();
var resistanceQuery = GetEntityQuery<RadiationGridResistanceComponent>();
var transformQuery = GetEntityQuery<TransformComponent>();
// precalculate world positions for each source
// so we won't need to calc this in cycle over and over again
var sourcesData = new ValueList<(RadiationSourceComponent, TransformComponent, Vector2)>();
foreach (var (source, sourceTrs) in sources)
{
var worldPos = _transform.GetWorldPosition(sourceTrs, transformQuery);
var data = (source, sourceTrs, worldPos);
sourcesData.Add(data);
}
// trace all rays from rad source to rad receivers
var rays = new List<RadiationRay>();
var receiversTotalRads = new ValueList<(RadiationReceiverComponent, float)>();
foreach (var (dest, destTrs) in destinations)
{
var destWorld = _transform.GetWorldPosition(destTrs, transformQuery);
var rads = 0f;
foreach (var (source, sourceTrs, sourceWorld) in sourcesData)
{
// send ray towards destination entity
var ray = Irradiate(sourceTrs.Owner, sourceTrs, sourceWorld,
destTrs.Owner, destTrs, destWorld,
source.Intensity, source.Slope, saveVisitedTiles, resistanceQuery);
if (ray == null)
continue;
// save ray for debug
rays.Add(ray);
// add rads to total rad exposure
if (ray.ReachedDestination)
rads += ray.Rads;
}
receiversTotalRads.Add((dest, rads));
}
// update information for debug overlay
var elapsedTime = stopwatch.Elapsed.TotalMilliseconds;
var totalSources = sourcesData.Count;
var totalReceivers = receiversTotalRads.Count;
UpdateGridcastDebugOverlay(elapsedTime, totalSources, totalReceivers, rays);
// send rads to each entity
foreach (var (receiver, rads) in receiversTotalRads)
{
// update radiation value of receiver
// if no radiation rays reached target, that will set it to 0
receiver.CurrentRadiation = rads;
// also send an event with combination of total rad
if (rads > 0)
IrradiateEntity(receiver.Owner, rads,GridcastUpdateRate);
}
}
private RadiationRay? Irradiate(EntityUid sourceUid, TransformComponent sourceTrs, Vector2 sourceWorld,
EntityUid destUid, TransformComponent destTrs, Vector2 destWorld,
float incomingRads, float slope, bool saveVisitedTiles,
EntityQuery<RadiationGridResistanceComponent> resistanceQuery)
{
// lets first check that source and destination on the same map
if (sourceTrs.MapID != destTrs.MapID)
return null;
var mapId = sourceTrs.MapID;
// get direction from rad source to destination and its distance
var dir = destWorld - sourceWorld;
var dist = dir.Length;
// check if receiver is too far away
if (dist > GridcastMaxDistance)
return null;
// will it even reach destination considering distance penalty
var rads = incomingRads - 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, sourceUid, sourceWorld, 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)
{
// todo: entity queries doesn't support interface - use it when IMapGridComponent will be removed
if (!TryComp(sourceTrs.GridUid.Value, out IMapGridComponent? gridComponent))
return ray;
return Gridcast(gridComponent.Grid, ray, saveVisitedTiles, resistanceQuery);
}
// 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 = _mapManager.FindGridsIntersecting(mapId, box, 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)
{
ray = Gridcast(grid, ray, saveVisitedTiles, resistanceQuery);
// looks like last grid blocked all radiation
// we can return right now
if (ray.Rads <= 0)
return ray;
}
return ray;
}
private RadiationRay Gridcast(IMapGrid grid, RadiationRay ray, bool saveVisitedTiles,
EntityQuery<RadiationGridResistanceComponent> resistanceQuery)
{
var blockers = new List<(Vector2i, float)>();
// if grid doesn't have resistance map just apply distance penalty
var gridUid = grid.GridEntityId;
if (!resistanceQuery.TryGetComponent(gridUid, out var resistance))
return ray;
var resistanceMap = resistance.ResistancePerTile;
// get coordinate of source and destination in grid coordinates
var sourceGrid = grid.TileIndicesFor(ray.Source);
var destGrid = grid.TileIndicesFor(ray.Destination);
// iterate tiles in grid line from source to destination
var line = new GridLineEnumerator(sourceGrid, destGrid);
while (line.MoveNext())
{
var point = line.Current;
if (!resistanceMap.TryGetValue(point, out var resData))
continue;
ray.Rads -= resData;
// save data for debug
if (saveVisitedTiles)
blockers.Add((point, ray.Rads));
// no intensity left after blocker
if (ray.Rads <= MinIntensity)
{
ray.Rads = 0;
break;
}
}
// save data for debug if needed
if (saveVisitedTiles && blockers.Count > 0)
ray.Blockers.Add(gridUid, blockers);
return ray;
}
}

View File

@@ -1,52 +1,46 @@
using Content.Shared.Radiation.Events; using Content.Shared.Radiation.Events;
using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
namespace Content.Server.Radiation.Systems; namespace Content.Server.Radiation.Systems;
public sealed class RadiationSystem : EntitySystem public sealed partial class RadiationSystem : EntitySystem
{ {
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private const float RadiationCooldown = 1.0f;
private float _accumulator; private float _accumulator;
public override void Initialize()
{
base.Initialize();
SubscribeCvars();
InitRadBlocking();
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeCvars();
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
_accumulator += frameTime; _accumulator += frameTime;
if (_accumulator < GridcastUpdateRate)
return;
while (_accumulator > RadiationCooldown) UpdateGridcast();
{ UpdateResistanceDebugOverlay();
_accumulator -= RadiationCooldown; _accumulator = 0f;
// All code here runs effectively every RadiationCooldown seconds, so use that as the "frame time".
foreach (var comp in EntityManager.EntityQuery<RadiationSourceComponent>())
{
var ent = comp.Owner;
if (Deleted(ent))
continue;
var cords = Transform(ent).MapPosition;
IrradiateRange(cords, comp.Range, comp.RadsPerSecond, RadiationCooldown);
}
}
}
public void IrradiateRange(MapCoordinates coordinates, float range, float radsPerSecond, float time)
{
var lookUp = _lookup.GetEntitiesInRange(coordinates, range);
foreach (var uid in lookUp)
{
if (Deleted(uid))
continue;
IrradiateEntity(uid, radsPerSecond, time);
}
} }
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time) public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time)
{ {
var msg = new OnIrradiatedEvent(time, radsPerSecond); var msg = new OnIrradiatedEvent(time, radsPerSecond);
RaiseLocalEvent(uid, msg, true); RaiseLocalEvent(uid, msg);
} }
} }

View File

@@ -623,6 +623,36 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<int> ExplosionSingleTickAreaLimit = public static readonly CVarDef<int> ExplosionSingleTickAreaLimit =
CVarDef.Create("explosion.single_tick_area_limit", 400, CVar.SERVERONLY); CVarDef.Create("explosion.single_tick_area_limit", 400, CVar.SERVERONLY);
/*
* Radiation
*/
/// <summary>
/// What is the smallest radiation dose in rads that can be received by object.
/// Extremely small values may impact performance.
/// </summary>
public static readonly CVarDef<float> RadiationMinIntensity =
CVarDef.Create("radiation.min_intensity", 0.1f, CVar.SERVERONLY);
/// <summary>
/// Rate of radiation system update in seconds.
/// </summary>
public static readonly CVarDef<float> RadiationGridcastUpdateRate =
CVarDef.Create("radiation.gridcast.update_rate", 1.0f, CVar.SERVERONLY);
/// <summary>
/// If both radiation source and receiver are placed on same grid, ignore grids between them.
/// May get inaccurate result in some cases, but greatly boost performance in general.
/// </summary>
public static readonly CVarDef<bool> RadiationGridcastSimplifiedSameGrid =
CVarDef.Create("radiation.gridcast.simplified_same_grid", true, CVar.SERVERONLY);
/// <summary>
/// Max distance that radiation ray can travel in meters.
/// </summary>
public static readonly CVarDef<float> RadiationGridcastMaxDistance =
CVarDef.Create("radiation.gridcast.max_distance", 50f, CVar.SERVERONLY);
/* /*
* Admin logs * Admin logs
*/ */

View File

@@ -1,3 +1,5 @@
namespace Content.Shared.Radiation.Components;
/// <summary> /// <summary>
/// Irradiate all objects in range. /// Irradiate all objects in range.
/// </summary> /// </summary>
@@ -5,16 +7,20 @@
public sealed class RadiationSourceComponent : Component public sealed class RadiationSourceComponent : Component
{ {
/// <summary> /// <summary>
/// How many rads per second receive irradiated object. /// Radiation intensity in center of the source in rads per second.
/// From there radiation rays will travel over distance and loose intensity
/// when hit radiation blocker.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("radsPerSecond")] [DataField("intensity")]
public float RadsPerSecond = 1; public float Intensity = 1;
/// <summary> /// <summary>
/// Radius of radiation source. /// Defines how fast radiation rays will loose intensity
/// over distance. The bigger the value, the shorter range
/// of radiation source will be.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("range")] [DataField("slope")]
public float Range = 5f; public float Slope = 0.5f;
} }

View File

@@ -0,0 +1,81 @@
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.Radiation.Events;
/// <summary>
/// Raised on server as networked event when radiation system update its state
/// and emitted all rays from rad sources towards rad receivers.
/// Contains debug information about rad rays and all blockers on their way.
/// </summary>
/// <remarks>
/// Will be sent only to clients that activated radiation view using console command.
/// </remarks>
[Serializable, NetSerializable]
public sealed class OnRadiationOverlayUpdateEvent : 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;
/// <summary>
/// Total count of entities with <see cref="RadiationSourceComponent"/> on all maps.
/// </summary>
public readonly int SourcesCount;
/// <summary>
/// Total count of entities with radiation receiver on all maps.
/// </summary>
public readonly int 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;
}
}
/// <summary>
/// Raised when server enabled/disabled radiation debug view for client.
/// After that client will start/stop receiving <see cref="OnRadiationOverlayUpdateEvent"/>.
/// </summary>
[Serializable, NetSerializable]
public sealed class OnRadiationOverlayToggledEvent : EntityEventArgs
{
/// <summary>
/// Does debug radiation view enabled.
/// </summary>
public readonly bool IsEnabled;
public OnRadiationOverlayToggledEvent(bool isEnabled)
{
IsEnabled = isEnabled;
}
}
/// <summary>
/// Raised when grid resistance was update for radiation overlay visualization.
/// </summary>
[Serializable, NetSerializable]
public sealed class OnRadiationOverlayResistanceUpdateEvent : EntityEventArgs
{
/// <summary>
/// Key is grids uid. Values are tiles with their rad resistance.
/// </summary>
public readonly Dictionary<EntityUid, Dictionary<Vector2i, float>> Grids;
public OnRadiationOverlayResistanceUpdateEvent(Dictionary<EntityUid, Dictionary<Vector2i, float>> grids)
{
Grids = grids;
}
}

View File

@@ -0,0 +1,64 @@
using Content.Shared.Radiation.Components;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Radiation.Systems;
/// <summary>
/// Ray emitted by radiation source towards radiation receiver.
/// Contains all information about encountered radiation blockers.
/// </summary>
[Serializable, NetSerializable]
public sealed class RadiationRay
{
/// <summary>
/// Map on which source and receiver are placed.
/// </summary>
public MapId MapId;
/// <summary>
/// Uid of entity with <see cref="RadiationSourceComponent"/>.
/// </summary>
public EntityUid SourceUid;
/// <summary>
/// World coordinates of radiation source.
/// </summary>
public Vector2 Source;
/// <summary>
/// Uid of entity with radiation receiver component.
/// </summary>
public EntityUid DestinationUid;
/// <summary>
/// World coordinates of radiation receiver.
/// </summary>
public Vector2 Destination;
/// <summary>
/// How many rads intensity reached radiation receiver.
/// </summary>
public float Rads;
/// <summary>
/// Has rad ray reached destination or lost all intensity after blockers?
/// </summary>
public bool ReachedDestination => Rads > 0;
/// <summary>
/// All blockers visited by gridcast. 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<EntityUid, List<(Vector2i, float)>> Blockers = new();
public RadiationRay(MapId mapId, EntityUid sourceUid, Vector2 source,
EntityUid destinationUid, Vector2 destination, float rads)
{
MapId = mapId;
SourceUid = sourceUid;
Source = source;
DestinationUid = destinationUid;
Destination = destination;
Rads = rads;
}
}

View File

@@ -25,8 +25,8 @@ public sealed class RadiationPulseSystem : EntitySystem
} }
// try to get radiation range or keep default visual range // try to get radiation range or keep default visual range
if (TryComp<RadiationSourceComponent>(uid, out var radSource)) if (TryComp<RadiationSourceComponent>(uid, out var radSource))
{ {
component.VisualRange = radSource.Range; component.VisualRange = radSource.Intensity / radSource.Slope;
} }
} }
} }

View File

@@ -1,5 +1,6 @@
using Content.Shared.Ghost; using Content.Shared.Ghost;
using Content.Shared.Radiation; using Content.Shared.Radiation;
using Content.Shared.Radiation.Components;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Collision.Shapes;
@@ -109,7 +110,7 @@ namespace Content.Shared.Singularity
if (EntityManager.TryGetComponent(singularity.Owner, out RadiationSourceComponent? source)) if (EntityManager.TryGetComponent(singularity.Owner, out RadiationSourceComponent? source))
{ {
source.RadsPerSecond = singularity.RadsPerLevel * value; source.Intensity = singularity.RadsPerLevel * value;
} }
if (EntityManager.TryGetComponent(singularity.Owner, out AppearanceComponent? appearance)) if (EntityManager.TryGetComponent(singularity.Owner, out AppearanceComponent? appearance))

View File

@@ -0,0 +1,2 @@
radiation-command-description = Toggle visibility of radiation rays coming from rad sources
radiation-command-help = Usage: showradiation

View File

@@ -5,7 +5,7 @@
description: Looking at this anomaly makes you feel strange, like something is pushing at your eyes. description: Looking at this anomaly makes you feel strange, like something is pushing at your eyes.
components: components:
- type: RadiationSource - type: RadiationSource
radsPerSecond: 5 intensity: 5
- type: TimedDespawn - type: TimedDespawn
lifetime: 2 lifetime: 2
- type: EmitSoundOnSpawn - type: EmitSoundOnSpawn

View File

@@ -69,6 +69,7 @@
-0.25 -0.25
- type: Damageable - type: Damageable
damageContainer: Biological damageContainer: Biological
- type: RadiationReceiver
- type: AtmosExposed - type: AtmosExposed
- type: Flammable - type: Flammable
fireSpread: true fireSpread: true

View File

@@ -178,6 +178,7 @@
preset: HumanPreset preset: HumanPreset
- type: Damageable - type: Damageable
damageContainer: Biological damageContainer: Biological
- type: RadiationReceiver
- type: ThermalRegulator - type: ThermalRegulator
metabolismHeat: 800 metabolismHeat: 800
radiatedHeat: 100 radiatedHeat: 100

View File

@@ -119,6 +119,8 @@
node: glassAirlock node: glassAirlock
- type: PaintableAirlock - type: PaintableAirlock
group: Windoor group: Windoor
- type: RadiationBlocker
resistance: 2
- type: entity - type: entity
parent: AirlockGlass parent: AirlockGlass

View File

@@ -79,6 +79,8 @@
- type: Airtight - type: Airtight
fixVacuum: true fixVacuum: true
noAirWhenFullyAirBlocked: false noAirWhenFullyAirBlocked: false
- type: RadiationBlocker
resistance: 3
- type: Occluder - type: Occluder
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic

View File

@@ -93,6 +93,8 @@
fixVacuum: true fixVacuum: true
airBlocked: false airBlocked: false
noAirWhenFullyAirBlocked: true noAirWhenFullyAirBlocked: true
- type: RadiationBlocker
enabled: false
- type: Occluder - type: Occluder
enabled: false enabled: false
- type: Construction - type: Construction

View File

@@ -25,6 +25,8 @@
- type: AirlockVisualizer - type: AirlockVisualizer
simpleVisuals: true simpleVisuals: true
animationTime: 1.0 animationTime: 1.0
- type: RadiationBlocker
resistance: 8
- type: entity - type: entity
id: BlastDoorOpen id: BlastDoorOpen
@@ -39,3 +41,5 @@
canCollide: false canCollide: false
- type: Airtight - type: Airtight
airBlocked: false airBlocked: false
- type: RadiationBlocker
enabled: false

View File

@@ -51,6 +51,8 @@
type: WiresBoundUserInterface type: WiresBoundUserInterface
- type: Airtight - type: Airtight
fixVacuum: true fixVacuum: true
- type: RadiationBlocker
resistance: 2
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
@@ -101,6 +103,8 @@
enabled: false enabled: false
- type: Airtight - type: Airtight
airBlocked: false airBlocked: false
- type: RadiationBlocker
enabled: false
- type: Construction - type: Construction
graph: Shutters graph: Shutters
node: Shutters node: Shutters
@@ -138,6 +142,8 @@
canCollide: false canCollide: false
- type: Airtight - type: Airtight
airBlocked: false airBlocked: false
- type: RadiationBlocker
enabled: false
- type: entity - type: entity
id: ShuttersWindow id: ShuttersWindow
@@ -169,6 +175,8 @@
canCollide: false canCollide: false
- type: Airtight - type: Airtight
airBlocked: false airBlocked: false
- type: RadiationBlocker
enabled: false
# Frame for construction # Frame for construction
- type: entity - type: entity

View File

@@ -47,6 +47,7 @@
- type: BatteryDischarger - type: BatteryDischarger
# This is JUST a default. It has to be dynamically adjusted to ensure that the battery doesn't discharge "too fast" & run out immediately, while still scaling by input power. # This is JUST a default. It has to be dynamically adjusted to ensure that the battery doesn't discharge "too fast" & run out immediately, while still scaling by input power.
activeSupplyRate: 100000 activeSupplyRate: 100000
- type: RadiationReceiver
- type: Anchorable - type: Anchorable
- type: Rotatable - type: Rotatable
- type: Pullable - type: Pullable

View File

@@ -24,10 +24,10 @@
layer: layer:
- AllMask - AllMask
- type: Singularity - type: Singularity
radsPerLevel: 1 radsPerLevel: 2
- type: SingularityDistortion - type: SingularityDistortion
- type: RadiationSource - type: RadiationSource
range: 10 slope: 0.2 # its emit really far away
- type: Sprite - type: Sprite
sprite: Structures/Power/Generation/Singularity/singularity_1.rsi sprite: Structures/Power/Generation/Singularity/singularity_1.rsi
state: singularity_1 state: singularity_1

View File

@@ -246,4 +246,4 @@
- state: rtg_damaged - state: rtg_damaged
- state: rtg_glow - state: rtg_glow
- type: RadiationSource # ideally only when opened. - type: RadiationSource # ideally only when opened.
range: 2 intensity: 2

View File

@@ -47,6 +47,8 @@
- type: Airtight - type: Airtight
- type: StaticPrice - type: StaticPrice
price: 75 price: 75
- type: RadiationBlocker
resistance: 2
- type: entity - type: entity
parent: BaseWall parent: BaseWall
@@ -451,6 +453,8 @@
- type: ReinforcedWallVisualizer - type: ReinforcedWallVisualizer
- type: StaticPrice - type: StaticPrice
price: 150 price: 150
- type: RadiationBlocker
resistance: 5
# Riveting # Riveting
- type: entity - type: entity

View File

@@ -41,6 +41,8 @@
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: StaticPrice - type: StaticPrice
price: 20.5 price: 20.5
- type: RadiationBlocker
resistance: 2
- type: entity - type: entity
id: PlasmaWindowDirectional id: PlasmaWindowDirectional

View File

@@ -11,6 +11,8 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: RGlass damageModifierSet: RGlass
- type: RadiationBlocker
resistance: 4
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -525,6 +525,8 @@ public sealed class $CLASS$ : Shared$CLASS$ {
<s:Boolean x:Key="/Default/UserDictionary/Words/=gamemode/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gamemode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gibs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Gibs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=godmode/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=godmode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gridcast/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Grindable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Grindable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hardcode/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=hardcode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hbox/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=hbox/@EntryIndexedValue">True</s:Boolean>