Explosion refactor (#5230)

* Explosions

* fix yaml typo

and prevent silly UI inputs

* oop

* Use modified contains() checks

And remove IEnumerable

* Buff nuke, nerf meteors

* optimize the entity lookup stuff a bit

* fix tile (0,0) error

forgot to do an initial Enumerator.MoveNext(), so the first tile was always the "null" tile.

* remove celebration

* byte -> int

* remove diag edge tile dict

* fix one bug

but there is another

* fix the other bug

turns out dividing a ushort leads to rounding errors.  Why TF is the grid tile size even a ushort in the first place.

* improve edge map

* fix minor bug

If the initial-explosion tile had an airtight entity on it, the tile was processed twice.

* some reviews (transform queries, eye.mapid, and tilesizes in overlays)

* Apply suggestions from code review

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* is map paused

* GetAllTiles ignores space by default

* WriteLine -> WriteError

* First -> FirstOrDefault()

* default prototype const string

* entity query

* misc review changes

* grid edge max distance

* fix fire texture defn

bad use of type serializer and ioc-resolves

* Remove ExplosionLaunched

And allow nukes to throw items towards the outer part of an explosion

* no hot-reload disclaimer

* replace prototype id string with int index

* optimise damage a tiiiiny bit.

* entity queries

* comments

* misc mirror comments

* cvars

* admin logs

* move intensity-per-state to prototype

* update tile event to ECS event

* git mv

* Tweak rpg & minibomb

also fix merge bug

* you don't exist anymore go away

* Fix build

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Leon Friedrich
2022-04-01 15:39:26 +13:00
committed by GitHub
parent 9a91536353
commit 56168e592e
70 changed files with 4209 additions and 770 deletions

View File

@@ -0,0 +1,193 @@
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using System.Linq;
namespace Content.Client.Administration.UI.SpawnExplosion;
[UsedImplicitly]
public sealed class ExplosionDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public Dictionary<int, List<Vector2i>>? SpaceTiles;
public Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles = new();
public List<float> Intensity = new();
public float TotalIntensity;
public float Slope;
public ushort SpaceTileSize;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
public Matrix3 SpaceMatrix;
public MapId Map;
private readonly Font _font;
public ExplosionDebugOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)
{
if (Map != args.Viewport.Eye?.Position.MapId)
return;
if (Tiles.Count == 0 && SpaceTiles == null)
return;
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawScreen(OverlayDrawArgs args)
{
var handle = args.ScreenHandle;
Box2 gridBounds;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var (gridId, tileSets) in Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
var gridXform = xformQuery.GetComponent(grid.GridEntityId);
var (_, _, matrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
gridBounds = invMatrix.TransformBox(args.WorldBounds);
DrawText(handle, gridBounds, matrix, tileSets, grid.TileSize);
}
if (SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
DrawText(handle, gridBounds, SpaceMatrix, SpaceTiles, SpaceTileSize);
}
private void DrawText(
DrawingHandleScreen handle,
Box2 gridBounds,
Matrix3 transform,
Dictionary<int, List<Vector2i>> tileSets,
ushort tileSize)
{
for (var i = 1; i < Intensity.Count; i++)
{
if (!tileSets.TryGetValue(i, out var tiles))
continue;
foreach (var tile in tiles)
{
var centre = ((Vector2) tile + 0.5f) * tileSize;
// is the center of this tile visible to the user?
if (!gridBounds.Contains(centre))
continue;
var worldCenter = transform.Transform(centre);
var screenCenter = _eyeManager.WorldToScreen(worldCenter);
if (Intensity![i] > 9)
screenCenter += (-12, -8);
else
screenCenter += (-8, -8);
handle.DrawString(_font, screenCenter, Intensity![i].ToString("F2"));
}
}
if (tileSets.ContainsKey(0))
{
var epicenter = tileSets[0].First();
var worldCenter = transform.Transform(((Vector2) epicenter + 0.5f) * tileSize);
var screenCenter = _eyeManager.WorldToScreen(worldCenter) + (-24, -24);
var text = $"{Intensity![0]:F2}\nΣ={TotalIntensity:F1}\nΔ={Slope:F1}";
handle.DrawString(_font, screenCenter, text);
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
Box2 gridBounds;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var (gridId, tileSets) in Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
var gridXform = xformQuery.GetComponent(grid.GridEntityId);
var (_, _, worldMatrix, invWorldMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
gridBounds = invWorldMatrix.TransformBox(args.WorldBounds);
handle.SetTransform(worldMatrix);
DrawTiles(handle, gridBounds, tileSets, SpaceTileSize);
}
if (SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
handle.SetTransform(SpaceMatrix);
DrawTiles(handle, gridBounds, SpaceTiles, SpaceTileSize);
}
private void DrawTiles(
DrawingHandleWorld handle,
Box2 gridBounds,
Dictionary<int, List<Vector2i>> tileSets,
ushort tileSize)
{
for (var i = 0; i < Intensity.Count; i++)
{
var color = ColorMap(Intensity![i]);
var colorTransparent = color;
colorTransparent.A = 0.2f;
if (!tileSets.TryGetValue(i, out var tiles))
continue;
foreach (var tile in tiles)
{
var centre = ((Vector2) tile + 0.5f) * tileSize;
// is the center of this tile visible to the user?
if (!gridBounds.Contains(centre))
continue;
var box = Box2.UnitCentered.Translated(centre);
handle.DrawRect(box, color, false);
handle.DrawRect(box, colorTransparent);
}
}
}
private Color ColorMap(float intensity)
{
var frac = 1 - intensity / Intensity![0];
Color result;
if (frac < 0.5f)
result = Color.InterpolateBetween(Color.Red, Color.Orange, frac * 2);
else
result = Color.InterpolateBetween(Color.Orange, Color.Yellow, (frac - 0.5f) * 2);
return result;
}
}

View File

@@ -0,0 +1,81 @@
using Content.Client.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Map;
namespace Content.Client.Administration.UI.SpawnExplosion;
[UsedImplicitly]
public sealed class SpawnExplosionEui : BaseEui
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
private readonly SpawnExplosionWindow _window;
private ExplosionDebugOverlay? _debugOverlay;
public SpawnExplosionEui()
{
IoCManager.InjectDependencies(this);
_window = new SpawnExplosionWindow(this);
_window.OnClose += SendClosedMessage;
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.OnClose -= SendClosedMessage;
_window.Close();
ClearOverlay();
}
public void SendClosedMessage()
{
SendMessage(new SpawnExplosionEuiMsg.Close());
}
public void ClearOverlay()
{
if (_overlayManager.HasOverlay<ExplosionDebugOverlay>())
_overlayManager.RemoveOverlay<ExplosionDebugOverlay>();
_debugOverlay = null;
}
public void RequestPreviewData(MapCoordinates epicenter, string typeId, float totalIntensity, float intensitySlope, float maxIntensity)
{
var msg = new SpawnExplosionEuiMsg.PreviewRequest(epicenter, typeId, totalIntensity, intensitySlope, maxIntensity);
SendMessage(msg);
}
/// <summary>
/// Receive explosion preview data and add a client-side explosion preview overlay
/// </summary>
/// <param name="msg"></param>
public override void HandleMessage(EuiMessageBase msg)
{
if (msg is not SpawnExplosionEuiMsg.PreviewData data)
return;
if (_debugOverlay == null)
{
_debugOverlay = new();
_overlayManager.AddOverlay(_debugOverlay);
}
_debugOverlay.Tiles = data.Explosion.Tiles;
_debugOverlay.SpaceTiles = data.Explosion.SpaceTiles;
_debugOverlay.Intensity = data.Explosion.Intensity;
_debugOverlay.Slope = data.Slope;
_debugOverlay.TotalIntensity = data.TotalIntensity;
_debugOverlay.Map = data.Explosion.Epicenter.MapId;
_debugOverlay.SpaceMatrix = data.Explosion.SpaceMatrix;
_debugOverlay.SpaceTileSize = data.Explosion.SpaceTileSize;
}
}

View File

@@ -0,0 +1,46 @@
<DefaultWindow
xmlns="https://spacestation14.io"
Title="{Loc 'admin-explosion-eui-title'}"
SetHeight="380">
<BoxContainer Name="MainContainer" Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-type'}" MinSize="120 0" />
<OptionButton Name="ExplosionOption" MinSize="70 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-mapid'}" MinSize="120 0" />
<OptionButton Name="MapOptions" MinSize="70 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-xmap'}" MinSize="120 0" />
<FloatSpinBox Name="MapX" MinSize="70 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-ymap'}" MinSize="120 0" />
<FloatSpinBox Name="MapY" MinSize="70 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="Recentre" Text="{Loc 'admin-explosion-eui-label-current'}" />
<Control MinSize="0 20"/>
<CheckBox Name="Preview" Text="{Loc 'admin-explosion-eui-label-preview'}"/>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-total'}" MinSize="120 0"/>
<FloatSpinBox Name="Intensity" MinSize="130 0" HorizontalExpand="True" Value="200"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-slope'}" MinSize="120 0" />
<FloatSpinBox Name="Slope" MinSize="130 0" HorizontalExpand="True" Value="5"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'admin-explosion-eui-label-max'}" MinSize="120 0" />
<FloatSpinBox Name="MaxIntensity" MinSize="130 0" HorizontalExpand="True" Value="100"/>
</BoxContainer>
<Control MinSize="0 20"/>
<Button Name="Spawn" Text="{Loc 'admin-explosion-eui-label-spawn'}" />
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,142 @@
using Content.Shared.Explosion;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.Player;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.OptionButton;
namespace Content.Client.Administration.UI.SpawnExplosion;
[GenerateTypedNameReferences]
[UsedImplicitly]
public sealed partial class SpawnExplosionWindow : DefaultWindow
{
[Dependency] private readonly IClientConsoleHost _conHost = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly SpawnExplosionEui _eui;
private List<MapId> _mapData = new();
private List<string> _explosionTypes = new();
/// <summary>
/// Used to prevent unnecessary preview updates when setting fields (e.g., updating position)..
/// </summary>
private bool _pausePreview;
public SpawnExplosionWindow(SpawnExplosionEui eui)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_eui = eui;
ExplosionOption.OnItemSelected += ExplosionSelected;
MapOptions.OnItemSelected += MapSelected;
Recentre.OnPressed += (_) => SetLocation();
Spawn.OnPressed += SubmitButtonOnOnPressed;
Preview.OnToggled += (_) => UpdatePreview();
MapX.OnValueChanged += (_) => UpdatePreview();
MapY.OnValueChanged += (_) => UpdatePreview();
Intensity.OnValueChanged += (_) => UpdatePreview();
Slope.OnValueChanged += (_) => UpdatePreview();
MaxIntensity.OnValueChanged += (_) => UpdatePreview();
}
private void ExplosionSelected(ItemSelectedEventArgs args)
{
ExplosionOption.SelectId(args.Id);
UpdatePreview();
}
private void MapSelected(ItemSelectedEventArgs args)
{
MapOptions.SelectId(args.Id);
UpdatePreview();
}
protected override void EnteredTree()
{
SetLocation();
UpdateExplosionTypeOptions();
}
private void UpdateExplosionTypeOptions()
{
_explosionTypes.Clear();
ExplosionOption.Clear();
foreach (var type in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
{
_explosionTypes.Add(type.ID);
ExplosionOption.AddItem(type.ID);
}
}
private void UpdateMapOptions()
{
_mapData.Clear();
MapOptions.Clear();
foreach (var map in _mapManager.GetAllMapIds())
{
_mapData.Add(map);
MapOptions.AddItem(map.ToString());
}
}
/// <summary>
/// Set the current grid & indices based on the attached entities current location.
/// </summary>
private void SetLocation()
{
UpdateMapOptions();
if (!_entMan.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out TransformComponent? transform))
return;
_pausePreview = true;
MapOptions.Select(_mapData.IndexOf(transform.MapID));
(MapX.Value, MapY.Value) = transform.MapPosition.Position;
_pausePreview = false;
UpdatePreview();
}
private void UpdatePreview()
{
if (_pausePreview)
return;
if (!Preview.Pressed)
{
_eui.ClearOverlay();
return;
}
MapCoordinates coords = new((MapX.Value, MapY.Value), _mapData[MapOptions.SelectedId]);
var explosionType = _explosionTypes[ExplosionOption.SelectedId];
_eui.RequestPreviewData(coords, explosionType, Intensity.Value, Slope.Value, MaxIntensity.Value);
}
private void SubmitButtonOnOnPressed(ButtonEventArgs args)
{
// need to make room to view the fireworks
Preview.Pressed = false;
_eui.ClearOverlay();
// for the actual explosion, we will just re-use the explosion command.
// so assemble command arguments:
var mapId = _mapData[MapOptions.SelectedId];
var explosionType = _explosionTypes[ExplosionOption.SelectedId];
var cmd = $"explosion {Intensity.Value} {Slope.Value} {MaxIntensity.Value} {MapX.Value} {MapY.Value} {mapId} {explosionType}";
_conHost.ExecuteCommand(cmd);
}
}

View File

@@ -16,6 +16,7 @@ namespace Content.Client.Entry
"Temperature",
"AtmosExposed",
"Explosive",
"ExplosionResistance",
"Vocal",
"OnUseTimerTrigger",
"WarpPoint",
@@ -267,7 +268,6 @@ namespace Content.Client.Entry
"RandomSpawner",
"SpawnAfterInteract",
"DisassembleOnAltVerb",
"ExplosionLaunched",
"BeingCloned",
"Advertise",
"Bible",

View File

@@ -0,0 +1,119 @@
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Client.Explosion;
[UsedImplicitly]
public sealed class ExplosionOverlay : Overlay
{
/// <summary>
/// The explosion that needs to be drawn. This explosion is currently being processed by the server and
/// expanding outwards.
/// </summary>
internal Explosion? ActiveExplosion;
/// <summary>
/// This index specifies what parts of the currently expanding explosion should be drawn.
/// </summary>
public int Index;
/// <summary>
/// These explosions have finished expanding, but we will draw for a few more frames. This is important for
/// small explosions, as otherwise they disappear far too quickly.
/// </summary>
internal List<Explosion> CompletedExplosions = new ();
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
private ShaderInstance _shader;
public ExplosionOverlay()
{
IoCManager.InjectDependencies(this);
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
{
var drawHandle = args.WorldHandle;
drawHandle.UseShader(_shader);
var xforms = _entMan.GetEntityQuery<TransformComponent>();
if (ActiveExplosion != null && ActiveExplosion.Map == args.Viewport.Eye?.Position.MapId)
{
DrawExplosion(drawHandle, args.WorldBounds, ActiveExplosion, Index, xforms);
}
foreach (var exp in CompletedExplosions)
{
if (exp.Map == args.Viewport.Eye?.Position.MapId)
DrawExplosion(drawHandle, args.WorldBounds, exp, exp.Intensity.Count, xforms);
}
drawHandle.SetTransform(Matrix3.Identity);
}
private void DrawExplosion(DrawingHandleWorld drawHandle, Box2Rotated worldBounds, Explosion exp, int index, EntityQuery<TransformComponent> xforms)
{
Box2 gridBounds;
foreach (var (gridId, tiles) in exp.Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
var xform = xforms.GetComponent(grid.GridEntityId);
var (_, _, worldMatrix, invWorldMatrix) = xform.GetWorldPositionRotationMatrixWithInv(xforms);
gridBounds = invWorldMatrix.TransformBox(worldBounds);
drawHandle.SetTransform(worldMatrix);
DrawTiles(drawHandle, gridBounds, index, tiles, exp, grid.TileSize);
}
if (exp.SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(exp.SpaceMatrix).TransformBox(worldBounds);
drawHandle.SetTransform(exp.SpaceMatrix);
DrawTiles(drawHandle, gridBounds, index, exp.SpaceTiles, exp, exp.SpaceTileSize);
}
private void DrawTiles(
DrawingHandleWorld drawHandle,
Box2 gridBounds,
int index,
Dictionary<int, List<Vector2i>> tileSets,
Explosion exp,
ushort tileSize)
{
for (var j = 0; j < index; j++)
{
if (!tileSets.TryGetValue(j, out var tiles))
continue;
var frameIndex = (int) Math.Min(exp.Intensity[j] / exp.IntensityPerState, exp.FireFrames.Count - 1);
var frames = exp.FireFrames[frameIndex];
foreach (var tile in tiles)
{
Vector2 centre = ((Vector2) tile + 0.5f) * tileSize;
if (!gridBounds.Contains(centre))
continue;
var texture = _robustRandom.Pick(frames);
drawHandle.DrawTextureRect(texture, Box2.CenteredAround(centre, (tileSize, tileSize)), exp.FireColor);
}
}
}
}

View File

@@ -0,0 +1,211 @@
using Content.Shared.CCVar;
using Content.Shared.Explosion;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Explosion;
/// <summary>
/// This system is responsible for showing the client-side explosion effects (light source & fire-overlay). The
/// fire overlay code is just a bastardized version of the atmos plasma fire overlay and uses the same texture.
/// </summary>
public sealed class ExplosionOverlaySystem : EntitySystem
{
private ExplosionOverlay _overlay = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
/// <summary>
/// For how many seconds should an explosion stay on-screen once it has finished expanding?
/// </summary>
public float ExplosionPersistence = 0.3f;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ExplosionEvent>(OnExplosion);
SubscribeNetworkEvent<ExplosionOverlayUpdateEvent>(HandleExplosionUpdate);
_cfg.OnValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence, true);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
_overlay = new ExplosionOverlay();
if (!overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.AddOverlay(_overlay);
}
private void SetExplosionPersistence(float value) => ExplosionPersistence = value;
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
// increment the lifetime of completed explosions, and remove them if they have been ons screen for more
// than ExplosionPersistence seconds
for (int i = _overlay.CompletedExplosions.Count - 1; i>= 0; i--)
{
var explosion = _overlay.CompletedExplosions[i];
if (_mapMan.IsMapPaused(explosion.Map))
continue;
explosion.Lifetime += frameTime;
if (explosion.Lifetime >= ExplosionPersistence)
{
EntityManager.QueueDeleteEntity(explosion.LightEntity);
// Remove-swap
_overlay.CompletedExplosions[i] = _overlay.CompletedExplosions[^1];
_overlay.CompletedExplosions.RemoveAt(_overlay.CompletedExplosions.Count - 1);
}
}
}
/// <summary>
/// The server has processed some explosion. This updates the client-side overlay so that the area covered
/// by the fire-visual matches up with the area that the explosion has affected.
/// </summary>
private void HandleExplosionUpdate(ExplosionOverlayUpdateEvent args)
{
if (args.ExplosionId != _overlay.ActiveExplosion?.Explosionid && !IsNewer(args.ExplosionId))
{
// out of order events. Ignore.
return;
}
_overlay.Index = args.Index;
if (_overlay.ActiveExplosion == null)
{
// no active explosion... events out of order?
return;
}
if (args.Index != int.MaxValue)
return;
// the explosion has finished expanding
_overlay.Index = 0;
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = null;
}
/// <summary>
/// A new explosion occurred. This prepares the client-side light entity and stores the
/// explosion/fire-effect overlay data.
/// </summary>
private void OnExplosion(ExplosionEvent args)
{
if (!_protoMan.TryIndex(args.TypeID, out ExplosionPrototype? type))
return;
// spawn in a light source at the epicenter
var lightEntity = Spawn("ExplosionLight", args.Epicenter);
var light = EnsureComp<PointLightComponent>(lightEntity);
light.Energy = light.Radius = args.Intensity.Count;
light.Color = type.LightColor;
if (_overlay.ActiveExplosion == null)
{
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
return;
}
// we have a currently active explosion. Can happen when events are received out of order. either multiple
// explosions are happening in one tick, or a new explosion was received before the event telling us the old one
// finished got through.
if (IsNewer(args.ExplosionId))
{
// This is a newer explosion. Add the old-currently-active explosions to the completed list
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
}
else
{
// explosions were out of order. keep the active one, and directly add the received one to the completed
// list.
_overlay.CompletedExplosions.Add(new(args, type, lightEntity, _resCache));
return;
}
}
public bool IsNewer(int explosionId)
{
if (_overlay.ActiveExplosion == null)
return true;
// If we ever get servers stable enough to live this long, the explosion Id int might overflow.
return _overlay.ActiveExplosion.Explosionid < explosionId
|| _overlay.ActiveExplosion.Explosionid > int.MaxValue/2 && explosionId < int.MinValue/2;
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.RemoveOverlay<ExplosionOverlay>();
}
}
internal sealed class Explosion
{
public readonly Dictionary<int, List<Vector2i>>? SpaceTiles;
public readonly Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles;
public readonly List<float> Intensity;
public readonly EntityUid LightEntity;
public readonly MapId Map;
public readonly int Explosionid;
public readonly ushort SpaceTileSize;
public readonly float IntensityPerState;
public readonly Matrix3 SpaceMatrix;
/// <summary>
/// How long have we been drawing this explosion, starting from the time the explosion was fully drawn.
/// </summary>
public float Lifetime;
/// <summary>
/// The textures used for the explosion fire effect. Each fire-state is associated with an explosion
/// intensity range, and each stat itself has several textures.
/// </summary>
public readonly List<Texture[]> FireFrames = new();
public readonly Color? FireColor;
internal Explosion(ExplosionEvent args, ExplosionPrototype type, EntityUid lightEntity, IResourceCache resCache)
{
Map = args.Epicenter.MapId;
SpaceTiles = args.SpaceTiles;
Tiles = args.Tiles;
Intensity = args.Intensity;
SpaceMatrix = args.SpaceMatrix;
Explosionid = args.ExplosionId;
FireColor = type.FireColor;
LightEntity = lightEntity;
SpaceTileSize = args.SpaceTileSize;
IntensityPerState = type.IntensityPerState;
var fireRsi = resCache.GetResource<RSIResource>(type.TexturePath).RSI;
foreach (var state in fireRsi)
{
FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
if (FireFrames.Count == type.FireStates)
break;
}
}
}