2024-08-09 08:24:05 +02:00
using System.Linq ;
2023-07-08 14:08:32 +10:00
using System.Numerics ;
2024-04-30 20:11:27 -07:00
using Content.Server.Atmos.EntitySystems ;
2024-08-09 08:24:05 +02:00
using Content.Server.Explosion.Components ;
2022-11-27 23:24:35 +13:00
using Content.Shared.CCVar ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Damage ;
2024-08-09 08:24:05 +02:00
using Content.Shared.Database ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Explosion ;
2024-02-01 07:29:01 -06:00
using Content.Shared.Explosion.Components ;
2024-08-04 20:15:07 -07:00
using Content.Shared.Explosion.EntitySystems ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Maps ;
using Content.Shared.Physics ;
2023-05-07 14:57:23 +12:00
using Content.Shared.Projectiles ;
using Content.Shared.Tag ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Map ;
2022-11-22 13:12:04 +11:00
using Robust.Shared.Map.Components ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Physics ;
2022-09-14 17:26:26 +10:00
using Robust.Shared.Physics.Components ;
2022-10-27 23:37:55 +11:00
using Robust.Shared.Physics.Dynamics ;
2024-08-09 08:24:05 +02:00
using Robust.Shared.Player ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Random ;
using Robust.Shared.Timing ;
2023-10-15 03:48:25 +11:00
using Robust.Shared.Utility ;
2023-09-30 14:35:32 +10:00
using TimedDespawnComponent = Robust . Shared . Spawners . TimedDespawnComponent ;
2024-09-19 00:01:40 +00:00
2022-04-01 15:39:26 +13:00
namespace Content.Server.Explosion.EntitySystems ;
2024-09-19 00:01:40 +00:00
public sealed partial class ExplosionSystem
2022-04-01 15:39:26 +13:00
{
2024-03-14 00:27:08 -04:00
[Dependency] private readonly FlammableSystem _flammableSystem = default ! ;
2022-04-01 15:39:26 +13:00
/// <summary>
/// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>.
/// </summary>
internal readonly Stopwatch Stopwatch = new ( ) ;
/// <summary>
/// How many tiles to explode before checking the stopwatch timer
/// </summary>
internal static int TileCheckIteration = 1 ;
/// <summary>
/// Queue for delayed processing of explosions. If there is an explosion that covers more than <see
/// cref="TilesPerTick"/> tiles, other explosions will actually be delayed slightly. Unless it's a station
/// nuke, this delay should never really be noticeable.
2024-03-29 23:46:05 +00:00
/// This is also used to combine explosion intensities of the same kind.
2022-04-01 15:39:26 +13:00
/// </summary>
2024-03-29 23:46:05 +00:00
private Queue < QueuedExplosion > _explosionQueue = new ( ) ;
/// <summary>
/// All queued explosions that will be processed in <see cref="_explosionQueue"/>.
/// These always have the same contents.
/// </summary>
private HashSet < QueuedExplosion > _queuedExplosions = new ( ) ;
2022-04-01 15:39:26 +13:00
/// <summary>
/// The explosion currently being processed.
/// </summary>
private Explosion ? _activeExplosion ;
2023-11-19 17:44:42 +00:00
/// <summary>
/// This list is used when raising <see cref="BeforeExplodeEvent"/> to avoid allocating a new list per event.
/// </summary>
private readonly List < EntityUid > _containedEntities = new ( ) ;
private readonly List < ( EntityUid , DamageSpecifier ) > _toDamage = new ( ) ;
2023-11-19 17:06:00 +13:00
private List < EntityUid > _anchored = new ( ) ;
2025-04-17 23:55:09 -04:00
private void OnMapRemoved ( MapRemovedEvent ev )
2022-04-05 19:22:35 +12:00
{
// If a map was deleted, check the explosion currently being processed belongs to that map.
2025-04-17 23:55:09 -04:00
if ( _activeExplosion ? . Epicenter . MapId ! = ev . MapId )
2022-04-05 19:22:35 +12:00
return ;
2022-11-27 23:24:35 +13:00
QueueDel ( _activeExplosion . VisualEnt ) ;
2022-04-05 19:22:35 +12:00
_activeExplosion = null ;
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = false ;
_pathfindingSystem . PauseUpdating = false ;
2022-04-05 19:22:35 +12:00
}
2022-04-01 15:39:26 +13:00
/// <summary>
/// Process the explosion queue.
/// </summary>
public override void Update ( float frameTime )
{
if ( _activeExplosion = = null & & _explosionQueue . Count = = 0 )
// nothing to do
return ;
Stopwatch . Restart ( ) ;
var x = Stopwatch . Elapsed . TotalMilliseconds ;
var tilesRemaining = TilesPerTick ;
while ( tilesRemaining > 0 & & MaxProcessingTime > Stopwatch . Elapsed . TotalMilliseconds )
{
// if there is no active explosion, get a new one to process
if ( _activeExplosion = = null )
{
// EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that
// there is at-least 1ms of time left before creating a new explosion
2024-03-14 00:27:08 -04:00
if ( MathF . Max ( MaxProcessingTime - 1 , 0.1f ) < Stopwatch . Elapsed . TotalMilliseconds )
2022-04-01 15:39:26 +13:00
break ;
2024-03-29 23:46:05 +00:00
if ( ! _explosionQueue . TryDequeue ( out var queued ) )
2022-04-01 15:39:26 +13:00
break ;
2024-03-29 23:46:05 +00:00
_queuedExplosions . Remove ( queued ) ;
_activeExplosion = SpawnExplosion ( queued ) ;
2022-04-01 15:39:26 +13:00
// explosion spawning can be null if something somewhere went wrong. (e.g., negative explosion
// intensity).
if ( _activeExplosion = = null )
continue ;
// just a lil nap
if ( SleepNodeSys )
{
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = true ;
_pathfindingSystem . PauseUpdating = true ;
2022-04-01 15:39:26 +13:00
// snooze grid-chunk regeneration?
// snooze power network (recipients look for new suppliers as wires get destroyed).
}
if ( _activeExplosion . Area > SingleTickAreaLimit )
break ; // start processing next turn.
}
// TODO EXPLOSION check if active explosion is on a paused map. If it is... I guess support swapping out &
// storing the "currently active" explosion?
2022-07-01 06:42:10 +12:00
#if EXCEPTION_TOLERANCE
try
{
#endif
2024-03-14 00:27:08 -04:00
var processed = _activeExplosion . Process ( tilesRemaining ) ;
tilesRemaining - = processed ;
2022-07-01 06:42:10 +12:00
2024-03-14 00:27:08 -04:00
// has the explosion finished processing?
if ( _activeExplosion . FinishedProcessing )
2022-11-27 23:24:35 +13:00
{
var comp = EnsureComp < TimedDespawnComponent > ( _activeExplosion . VisualEnt ) ;
comp . Lifetime = _cfg . GetCVar ( CCVars . ExplosionPersistence ) ;
_appearance . SetData ( _activeExplosion . VisualEnt , ExplosionAppearanceData . Progress , int . MaxValue ) ;
_activeExplosion = null ;
}
2022-07-01 06:42:10 +12:00
#if EXCEPTION_TOLERANCE
}
2025-05-08 10:01:02 +10:00
catch ( Exception )
2022-07-01 06:42:10 +12:00
{
// Ensure the system does not get stuck in an error-loop.
2022-11-27 23:24:35 +13:00
if ( _activeExplosion ! = null )
QueueDel ( _activeExplosion . VisualEnt ) ;
2022-04-01 15:39:26 +13:00
_activeExplosion = null ;
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = false ;
_pathfindingSystem . PauseUpdating = false ;
2022-08-07 16:01:21 +12:00
throw ;
2022-07-01 06:42:10 +12:00
}
#endif
2022-04-01 15:39:26 +13:00
}
2024-03-10 01:15:13 +01:00
Log . Info ( $"Processed {TilesPerTick - tilesRemaining} tiles in {Stopwatch.Elapsed.TotalMilliseconds}ms" ) ;
2022-04-01 15:39:26 +13:00
// we have finished processing our tiles. Is there still an ongoing explosion?
if ( _activeExplosion ! = null )
{
2022-11-27 23:24:35 +13:00
_appearance . SetData ( _activeExplosion . VisualEnt , ExplosionAppearanceData . Progress , _activeExplosion . CurrentIteration + 1 ) ;
2022-04-01 15:39:26 +13:00
return ;
}
if ( _explosionQueue . Count > 0 )
return ;
//wakey wakey
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = false ;
_pathfindingSystem . PauseUpdating = false ;
2022-04-01 15:39:26 +13:00
}
/// <summary>
/// Determines whether an entity is blocking a tile or not. (whether it can prevent the tile from being uprooted
/// by an explosion).
/// </summary>
/// <remarks>
/// Used for a variation of <see cref="TurfHelpers.IsBlockedTurf()"/> that makes use of the fact that we have
/// already done an entity lookup on a tile, and don't need to do so again.
/// </remarks>
2023-10-15 03:48:25 +11:00
public bool IsBlockingTurf ( EntityUid uid )
2022-04-01 15:39:26 +13:00
{
if ( EntityManager . IsQueuedForDeletion ( uid ) )
return false ;
2023-10-15 03:48:25 +11:00
if ( ! _physicsQuery . TryGetComponent ( uid , out var physics ) )
2022-04-01 15:39:26 +13:00
return false ;
return physics . CanCollide & & physics . Hard & & ( physics . CollisionLayer & ( int ) CollisionGroup . Impassable ) ! = 0 ;
}
/// <summary>
/// Find entities on a grid tile using the EntityLookupComponent and apply explosion effects.
/// </summary>
/// <returns>True if the underlying tile can be uprooted, false if the tile is blocked by a dense entity</returns>
2022-10-27 23:37:55 +11:00
internal bool ExplodeTile ( BroadphaseComponent lookup ,
2023-11-19 17:06:00 +13:00
Entity < MapGridComponent > grid ,
2022-04-01 15:39:26 +13:00
Vector2i tile ,
float throwForce ,
DamageSpecifier damage ,
MapCoordinates epicenter ,
HashSet < EntityUid > processed ,
2024-03-14 00:27:08 -04:00
string id ,
2024-08-09 08:24:05 +02:00
float? fireStacks ,
EntityUid ? cause )
2022-04-01 15:39:26 +13:00
{
2023-11-19 17:06:00 +13:00
var size = grid . Comp . TileSize ;
var gridBox = new Box2 ( tile * size , ( tile + 1 ) * size ) ;
2022-04-01 15:39:26 +13:00
// get the entities on a tile. Note that we cannot process them directly, or we get
// enumerator-changed-while-enumerating errors.
2023-10-15 03:48:25 +11:00
List < ( EntityUid , TransformComponent ) > list = new ( ) ;
2024-09-19 00:01:40 +00:00
var state = ( list , processed , EntityManager . TransformQuery ) ;
2022-04-01 15:39:26 +13:00
2022-07-08 15:29:43 +12:00
// get entities:
2022-10-27 23:37:55 +11:00
lookup . DynamicTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
lookup . StaticTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
lookup . SundriesTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
2022-10-28 14:57:00 +13:00
lookup . StaticSundriesTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
2022-04-01 15:39:26 +13:00
// process those entities
2023-10-15 03:48:25 +11:00
foreach ( var ( uid , xform ) in list )
2022-04-01 15:39:26 +13:00
{
2024-08-09 08:24:05 +02:00
ProcessEntity ( uid , epicenter , damage , throwForce , id , xform , fireStacks , cause ) ;
2022-04-01 15:39:26 +13:00
}
// process anchored entities
var tileBlocked = false ;
2023-11-19 17:06:00 +13:00
_anchored . Clear ( ) ;
_map . GetAnchoredEntities ( grid , tile , _anchored ) ;
foreach ( var entity in _anchored )
2022-04-01 15:39:26 +13:00
{
processed . Add ( entity ) ;
2024-08-09 08:24:05 +02:00
ProcessEntity ( entity , epicenter , damage , throwForce , id , null , fireStacks , cause ) ;
2022-04-09 09:07:02 +12:00
}
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
// information about the entities that were spawned as a result, but without that information we just have to
// re-check for new anchored entities. Compared to entity spawning & deleting, this should still be relatively minor.
2023-11-19 17:06:00 +13:00
if ( _anchored . Count > 0 )
2022-04-09 09:07:02 +12:00
{
2023-11-19 17:06:00 +13:00
_anchored . Clear ( ) ;
_map . GetAnchoredEntities ( grid , tile , _anchored ) ;
foreach ( var entity in _anchored )
2022-04-09 09:07:02 +12:00
{
2023-10-15 03:48:25 +11:00
tileBlocked | = IsBlockingTurf ( entity ) ;
2022-04-09 09:07:02 +12:00
}
2022-04-01 15:39:26 +13:00
}
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
// windows will be flung outwards, and not stay where they spawned. This is however somewhat unnecessary, and a
// prime candidate for computational cost-cutting. Alternatively, it would be nice if there was just some sort
// of spawned-on-destruction event that could be used to automatically assemble a list of new entities that need
// to be thrown.
//
// All things considered, until entity spawning & destruction is sped up, this isn't all that time consuming.
// And throwing is disabled for nukes anyways.
if ( throwForce < = 0 )
return ! tileBlocked ;
list . Clear ( ) ;
2022-10-27 23:37:55 +11:00
lookup . DynamicTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
lookup . SundriesTree . QueryAabb ( ref state , GridQueryCallback , gridBox , true ) ;
2022-04-01 15:39:26 +13:00
2023-10-15 03:48:25 +11:00
foreach ( var ( uid , xform ) in list )
2022-04-01 15:39:26 +13:00
{
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
// they should handle their own damage pass-through, with their own damage reduction calculation.
2024-08-09 08:24:05 +02:00
ProcessEntity ( uid , epicenter , null , throwForce , id , xform , null , cause ) ;
2022-04-01 15:39:26 +13:00
}
return ! tileBlocked ;
}
2023-11-28 20:02:24 -05:00
private static bool GridQueryCallback (
2023-10-15 03:48:25 +11:00
ref ( List < ( EntityUid , TransformComponent ) > List , HashSet < EntityUid > Processed , EntityQuery < TransformComponent > XformQuery ) state ,
2022-07-08 15:29:43 +12:00
in EntityUid uid )
{
if ( state . Processed . Add ( uid ) & & state . XformQuery . TryGetComponent ( uid , out var xform ) )
2023-10-15 03:48:25 +11:00
state . List . Add ( ( uid , xform ) ) ;
2022-07-08 15:29:43 +12:00
return true ;
}
2023-11-28 20:02:24 -05:00
private static bool GridQueryCallback (
2023-10-15 03:48:25 +11:00
ref ( List < ( EntityUid , TransformComponent ) > List , HashSet < EntityUid > Processed , EntityQuery < TransformComponent > XformQuery ) state ,
2022-10-27 23:37:55 +11:00
in FixtureProxy proxy )
{
2023-05-09 19:21:26 +12:00
var owner = proxy . Entity ;
2022-10-27 23:37:55 +11:00
return GridQueryCallback ( ref state , in owner ) ;
}
2022-04-01 15:39:26 +13:00
/// <summary>
/// Same as <see cref="ExplodeTile"/>, but for SPAAAAAAACE.
/// </summary>
2025-05-24 10:55:46 -04:00
internal void ExplodeSpace ( Entity < BroadphaseComponent > lookup ,
2024-06-02 05:07:41 +01:00
Matrix3x2 spaceMatrix ,
Matrix3x2 invSpaceMatrix ,
2022-04-01 15:39:26 +13:00
Vector2i tile ,
float throwForce ,
DamageSpecifier damage ,
MapCoordinates epicenter ,
HashSet < EntityUid > processed ,
2024-03-14 00:27:08 -04:00
string id ,
2024-08-09 08:24:05 +02:00
float? fireStacks ,
EntityUid ? cause )
2022-04-01 15:39:26 +13:00
{
2023-07-08 14:08:32 +10:00
var gridBox = Box2 . FromDimensions ( tile * DefaultTileSize , new Vector2 ( DefaultTileSize , DefaultTileSize ) ) ;
2022-04-01 15:39:26 +13:00
var worldBox = spaceMatrix . TransformBox ( gridBox ) ;
2023-10-15 03:48:25 +11:00
var list = new List < ( EntityUid , TransformComponent ) > ( ) ;
2024-09-19 00:01:40 +00:00
var state = ( list , processed , invSpaceMatrix , lookup . Owner , EntityManager . TransformQuery , gridBox , _transformSystem ) ;
2022-04-01 15:39:26 +13:00
2022-07-08 15:29:43 +12:00
// get entities:
2025-05-24 10:55:46 -04:00
lookup . Comp . DynamicTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
lookup . Comp . StaticTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
lookup . Comp . SundriesTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
lookup . Comp . StaticSundriesTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
2022-04-01 15:39:26 +13:00
2023-10-15 03:48:25 +11:00
foreach ( var ( uid , xform ) in state . Item1 )
2022-04-01 15:39:26 +13:00
{
2023-10-15 03:48:25 +11:00
processed . Add ( uid ) ;
2024-08-09 08:24:05 +02:00
ProcessEntity ( uid , epicenter , damage , throwForce , id , xform , fireStacks , cause ) ;
2022-04-01 15:39:26 +13:00
}
if ( throwForce < = 0 )
return ;
// Also, throw any entities that were spawned as shrapnel. Compared to entity spawning & destruction, this extra
// lookup is relatively minor computational cost, and throwing is disabled for nukes anyways.
list . Clear ( ) ;
2025-05-24 10:55:46 -04:00
lookup . Comp . DynamicTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
lookup . Comp . SundriesTree . QueryAabb ( ref state , SpaceQueryCallback , worldBox , true ) ;
2022-07-08 15:29:43 +12:00
2023-10-15 03:48:25 +11:00
foreach ( var ( uid , xform ) in list )
2022-04-01 15:39:26 +13:00
{
2024-08-09 08:24:05 +02:00
ProcessEntity ( uid , epicenter , null , throwForce , id , xform , fireStacks , cause ) ;
2022-04-01 15:39:26 +13:00
}
}
2023-11-28 20:02:24 -05:00
private static bool SpaceQueryCallback (
2024-06-02 05:07:41 +01:00
ref ( List < ( EntityUid , TransformComponent ) > List , HashSet < EntityUid > Processed , Matrix3x2 InvSpaceMatrix , EntityUid LookupOwner , EntityQuery < TransformComponent > XformQuery , Box2 GridBox , SharedTransformSystem System ) state ,
2022-07-08 15:29:43 +12:00
in EntityUid uid )
{
if ( state . Processed . Contains ( uid ) )
return true ;
var xform = state . XformQuery . GetComponent ( uid ) ;
if ( xform . ParentUid = = state . LookupOwner )
{
// parented directly to the map, use local position
2024-06-02 05:07:41 +01:00
if ( state . GridBox . Contains ( Vector2 . Transform ( xform . LocalPosition , state . InvSpaceMatrix ) ) )
2023-10-15 03:48:25 +11:00
state . List . Add ( ( uid , xform ) ) ;
2022-07-08 15:29:43 +12:00
return true ;
}
// finally check if it intersects our tile
2023-11-28 20:02:24 -05:00
var wpos = state . System . GetWorldPosition ( xform ) ;
2024-06-02 05:07:41 +01:00
if ( state . GridBox . Contains ( Vector2 . Transform ( wpos , state . InvSpaceMatrix ) ) )
2023-10-15 03:48:25 +11:00
state . List . Add ( ( uid , xform ) ) ;
2022-07-08 15:29:43 +12:00
return true ;
}
2023-11-28 20:02:24 -05:00
private static bool SpaceQueryCallback (
2024-06-02 05:07:41 +01:00
ref ( List < ( EntityUid , TransformComponent ) > List , HashSet < EntityUid > Processed , Matrix3x2 InvSpaceMatrix , EntityUid LookupOwner , EntityQuery < TransformComponent > XformQuery , Box2 GridBox , SharedTransformSystem System ) state ,
2022-10-27 23:37:55 +11:00
in FixtureProxy proxy )
{
2023-05-09 19:21:26 +12:00
var uid = proxy . Entity ;
return SpaceQueryCallback ( ref state , in uid ) ;
2022-10-27 23:37:55 +11:00
}
2023-11-19 17:44:42 +00:00
private DamageSpecifier GetDamage ( EntityUid uid ,
string id , DamageSpecifier damage )
2022-04-01 15:39:26 +13:00
{
2023-11-19 17:44:42 +00:00
// TODO Explosion Performance
// Cache this? I.e., instead of raising an event, check for a component?
var resistanceEv = new GetExplosionResistanceEvent ( id ) ;
RaiseLocalEvent ( uid , ref resistanceEv ) ;
resistanceEv . DamageCoefficient = Math . Max ( 0 , resistanceEv . DamageCoefficient ) ;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if ( resistanceEv . DamageCoefficient ! = 1 )
damage * = resistanceEv . DamageCoefficient ;
return damage ;
}
private void GetEntitiesToDamage ( EntityUid uid , DamageSpecifier originalDamage , string prototype )
{
_toDamage . Clear ( ) ;
2023-12-11 09:43:00 +00:00
// don't raise BeforeExplodeEvent if the entity is completely immune to explosions
var thisDamage = GetDamage ( uid , prototype , originalDamage ) ;
2024-05-31 14:28:11 +12:00
if ( thisDamage . Empty )
2023-12-11 09:43:00 +00:00
return ;
_toDamage . Add ( ( uid , thisDamage ) ) ;
2022-04-01 15:39:26 +13:00
2023-11-19 17:44:42 +00:00
for ( var i = 0 ; i < _toDamage . Count ; i + + )
{
var ( ent , damage ) = _toDamage [ i ] ;
_containedEntities . Clear ( ) ;
var ev = new BeforeExplodeEvent ( damage , prototype , _containedEntities ) ;
RaiseLocalEvent ( ent , ref ev ) ;
2022-07-06 23:15:20 -04:00
2023-11-19 17:44:42 +00:00
if ( _containedEntities . Count = = 0 )
continue ;
2023-10-15 03:48:25 +11:00
// ReSharper disable once CompareOfFloatsByEqualityOperator
if ( ev . DamageCoefficient ! = 1 )
damage * = ev . DamageCoefficient ;
2023-11-19 17:44:42 +00:00
_toDamage . EnsureCapacity ( _toDamage . Count + _containedEntities . Count ) ;
foreach ( var contained in _containedEntities )
2022-04-01 15:39:26 +13:00
{
2023-11-19 17:44:42 +00:00
var newDamage = GetDamage ( contained , prototype , damage ) ;
_toDamage . Add ( ( contained , newDamage ) ) ;
2022-04-01 15:39:26 +13:00
}
}
2023-11-19 17:44:42 +00:00
}
2022-04-01 15:39:26 +13:00
2023-11-19 17:44:42 +00:00
/// <summary>
/// This function actually applies the explosion affects to an entity.
/// </summary>
private void ProcessEntity (
EntityUid uid ,
MapCoordinates epicenter ,
DamageSpecifier ? originalDamage ,
float throwForce ,
string id ,
2024-03-14 00:27:08 -04:00
TransformComponent ? xform ,
2024-08-09 08:24:05 +02:00
float? fireStacksOnIgnite ,
EntityUid ? cause )
2023-11-19 17:44:42 +00:00
{
if ( originalDamage ! = null )
2023-11-13 22:57:52 +00:00
{
2023-11-19 17:44:42 +00:00
GetEntitiesToDamage ( uid , originalDamage , id ) ;
foreach ( var ( entity , damage ) in _toDamage )
2023-11-13 22:57:52 +00:00
{
2024-08-09 08:24:05 +02:00
if ( damage . GetTotal ( ) > 0 & & TryComp < ActorComponent > ( entity , out var actorComponent ) )
{
// Log damage to player entities only, cause this will create a massive amount of log spam otherwise.
if ( cause ! = null )
{
_adminLogger . Add ( LogType . ExplosionHit , LogImpact . Medium , $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}" ) ;
}
else
{
_adminLogger . Add ( LogType . ExplosionHit , LogImpact . Medium , $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}" ) ;
}
}
2023-11-19 17:44:42 +00:00
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
2025-02-18 08:28:42 +01:00
_damageableSystem . TryChangeDamage ( entity , damage * _damageableSystem . UniversalExplosionDamageModifier , ignoreResistances : true ) ;
2024-03-14 00:27:08 -04:00
}
}
// ignite
if ( fireStacksOnIgnite ! = null )
{
if ( _flammableQuery . TryGetComponent ( uid , out var flammable ) )
{
flammable . FireStacks + = fireStacksOnIgnite . Value ;
_flammableSystem . Ignite ( uid , uid , flammable ) ;
2023-11-13 22:57:52 +00:00
}
}
2022-04-01 15:39:26 +13:00
// throw
2023-11-19 17:44:42 +00:00
if ( xform ! = null // null implies anchored or in a container
2022-04-01 15:39:26 +13:00
& & ! xform . Anchored
& & throwForce > 0
& & ! EntityManager . IsQueuedForDeletion ( uid )
2023-10-15 03:48:25 +11:00
& & _physicsQuery . TryGetComponent ( uid , out var physics )
2022-04-01 15:39:26 +13:00
& & physics . BodyType = = BodyType . Dynamic )
{
2023-10-15 03:48:25 +11:00
var pos = _transformSystem . GetWorldPosition ( xform ) ;
2024-10-04 14:43:45 +06:00
var dir = pos - epicenter . Position ;
if ( dir . IsLengthZero ( ) )
dir = _robustRandom . NextVector2 ( ) . Normalized ( ) ;
2023-05-07 14:57:23 +12:00
_throwingSystem . TryThrow (
uid ,
2024-10-04 14:43:45 +06:00
dir ,
2023-05-07 14:57:23 +12:00
physics ,
xform ,
2023-10-15 03:48:25 +11:00
_projectileQuery ,
2023-05-07 14:57:23 +12:00
throwForce ) ;
2022-04-01 15:39:26 +13:00
}
}
/// <summary>
/// Tries to damage floor tiles. Not to be confused with the function that damages entities intersecting the
/// grid tile.
/// </summary>
public void DamageFloorTile ( TileRef tileRef ,
2022-04-05 19:22:35 +12:00
float effectiveIntensity ,
int maxTileBreak ,
2022-04-09 09:07:02 +12:00
bool canCreateVacuum ,
2022-04-01 15:39:26 +13:00
List < ( Vector2i GridIndices , Tile Tile ) > damagedTiles ,
ExplosionPrototype type )
{
2025-01-11 13:27:08 -04:00
if ( _tileDefinitionManager [ tileRef . Tile . TypeId ] is not ContentTileDefinition tileDef
| | tileDef . Indestructible )
2022-04-09 09:07:02 +12:00
return ;
2024-04-30 20:11:27 -07:00
if ( ! CanCreateVacuum )
canCreateVacuum = false ;
else if ( tileDef . MapAtmosphere )
2022-04-09 09:07:02 +12:00
canCreateVacuum = true ; // is already a vacuum.
2022-04-01 15:39:26 +13:00
2022-04-05 19:22:35 +12:00
int tileBreakages = 0 ;
while ( maxTileBreak > tileBreakages & & _robustRandom . Prob ( type . TileBreakChance ( effectiveIntensity ) ) )
2022-04-01 15:39:26 +13:00
{
2022-04-05 19:22:35 +12:00
tileBreakages + + ;
effectiveIntensity - = type . TileBreakRerollReduction ;
2022-04-01 15:39:26 +13:00
2022-04-09 09:07:02 +12:00
// does this have a base-turf that we can break it down to?
2023-05-19 08:10:56 +01:00
if ( string . IsNullOrEmpty ( tileDef . BaseTurf ) )
2022-04-01 15:39:26 +13:00
break ;
2023-05-19 08:10:56 +01:00
if ( _tileDefinitionManager [ tileDef . BaseTurf ] is not ContentTileDefinition newDef )
2022-04-09 09:07:02 +12:00
break ;
2024-03-24 03:34:56 +11:00
if ( newDef . MapAtmosphere & & ! canCreateVacuum )
2022-04-01 15:39:26 +13:00
break ;
2022-04-09 09:07:02 +12:00
tileDef = newDef ;
2022-04-01 15:39:26 +13:00
}
if ( tileDef . TileId = = tileRef . Tile . TypeId )
return ;
damagedTiles . Add ( ( tileRef . GridIndices , new Tile ( tileDef . TileId ) ) ) ;
}
}
/// <summary>
/// This is a data class that stores information about the area affected by an explosion, for processing by <see
/// cref="ExplosionSystem"/>.
/// </summary>
/// <remarks>
2022-04-05 19:22:35 +12:00
/// This is basically the output of <see cref="ExplosionSystem.GetExplosionTiles()"/>, but with some utility functions for
/// iterating over the tiles, along with the ability to keep track of what entities have already been damaged by
2022-04-01 15:39:26 +13:00
/// this explosion.
/// </remarks>
sealed class Explosion
{
2022-04-05 19:22:35 +12:00
/// <summary>
/// For every grid (+ space) that the explosion reached, this data struct stores information about the tiles and
/// caches the entity-lookup component so that it doesn't have to be re-fetched for every tile.
/// </summary>
2022-04-01 15:39:26 +13:00
struct ExplosionData
{
2022-04-05 19:22:35 +12:00
/// <summary>
/// The tiles that the explosion damaged, grouped by the iteration (can be thought of as the distance from the epicenter)
/// </summary>
2022-04-01 15:39:26 +13:00
public Dictionary < int , List < Vector2i > > TileLists ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// Lookup component for this grid (or space/map).
/// </summary>
2025-05-24 10:55:46 -04:00
public Entity < BroadphaseComponent > Lookup ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// The actual grid that this corresponds to. If null, this implies space.
/// </summary>
2025-05-24 10:55:46 -04:00
public Entity < MapGridComponent > ? MapGrid ;
2022-04-01 15:39:26 +13:00
}
2022-04-05 19:22:35 +12:00
private readonly List < ExplosionData > _explosionData = new ( ) ;
/// <summary>
/// The explosion intensity associated with each tile iteration.
/// </summary>
private readonly List < float > _tileSetIntensity ;
2022-04-01 15:39:26 +13:00
/// <summary>
/// Used to avoid applying explosion effects repeatedly to the same entity. Particularly important if the
/// explosion throws this entity, as then it will be moving while the explosion is happening.
/// </summary>
public readonly HashSet < EntityUid > ProcessedEntities = new ( ) ;
/// <summary>
2022-04-06 19:35:18 +10:00
/// This integer tracks how much of this explosion has been processed.
2022-04-01 15:39:26 +13:00
/// </summary>
public int CurrentIteration { get ; private set ; } = 0 ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// The prototype for this explosion. Determines tile break chance, damage, etc.
/// </summary>
2022-04-01 15:39:26 +13:00
public readonly ExplosionPrototype ExplosionType ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// The center of the explosion. Used for physics throwing. Also used to identify the map on which the explosion is happening.
/// </summary>
2022-04-01 15:39:26 +13:00
public readonly MapCoordinates Epicenter ;
2022-04-05 19:22:35 +12:00
/// <summary>
2022-07-08 15:29:43 +12:00
/// The matrix that defines the reference frame for the explosion in space.
2022-04-05 19:22:35 +12:00
/// </summary>
2024-06-02 05:07:41 +01:00
private readonly Matrix3x2 _spaceMatrix ;
2022-04-01 15:39:26 +13:00
2022-04-05 19:22:35 +12:00
/// <summary>
/// Inverse of <see cref="_spaceMatrix"/>
/// </summary>
2024-06-02 05:07:41 +01:00
private readonly Matrix3x2 _invSpaceMatrix ;
2022-04-01 15:39:26 +13:00
2022-04-05 19:22:35 +12:00
/// <summary>
/// Have all the tiles on all the grids been processed?
/// </summary>
2022-04-01 15:39:26 +13:00
public bool FinishedProcessing ;
2022-04-05 19:22:35 +12:00
// Variables used for enumerating over tiles, grids, etc
2022-04-01 15:39:26 +13:00
private DamageSpecifier _currentDamage = default ! ;
2023-10-15 03:48:25 +11:00
#if DEBUG
private DamageSpecifier ? _expectedDamage ;
#endif
2025-05-24 10:55:46 -04:00
private Entity < BroadphaseComponent > _currentLookup = default ! ;
private Entity < MapGridComponent > ? _currentGrid ;
2022-04-01 15:39:26 +13:00
private float _currentIntensity ;
private float _currentThrowForce ;
private List < Vector2i > . Enumerator _currentEnumerator ;
private int _currentDataIndex ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// The set of tiles that need to be updated when the explosion has finished processing. Used to avoid having
/// the explosion trigger chunk regeneration & shuttle-system processing every tick.
/// </summary>
2025-05-24 10:55:46 -04:00
private readonly Dictionary < Entity < MapGridComponent > , List < ( Vector2i , Tile ) > > _tileUpdateDict = new ( ) ;
2022-04-05 19:22:35 +12:00
// Entity Queries
private readonly EntityQuery < TransformComponent > _xformQuery ;
private readonly EntityQuery < PhysicsComponent > _physicsQuery ;
private readonly EntityQuery < DamageableComponent > _damageQuery ;
2023-05-07 14:57:23 +12:00
private readonly EntityQuery < ProjectileComponent > _projectileQuery ;
private readonly EntityQuery < TagComponent > _tagQuery ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// Total area that the explosion covers.
/// </summary>
public readonly int Area ;
/// <summary>
/// factor used to scale the tile break chances.
/// </summary>
private readonly float _tileBreakScale ;
2022-04-01 15:39:26 +13:00
2022-04-05 19:22:35 +12:00
/// <summary>
/// Maximum number of times that an explosion will break a single tile.
/// </summary>
private readonly int _maxTileBreak ;
2022-04-01 15:39:26 +13:00
2022-04-09 09:07:02 +12:00
/// <summary>
/// Whether this explosion can turn non-vacuum tiles into vacuum-tiles.
/// </summary>
private readonly bool _canCreateVacuum ;
2022-04-05 19:22:35 +12:00
private readonly IEntityManager _entMan ;
2022-04-01 15:39:26 +13:00
private readonly ExplosionSystem _system ;
2024-09-29 02:27:47 +03:00
private readonly SharedMapSystem _mapSystem ;
2022-04-01 15:39:26 +13:00
2022-11-27 23:24:35 +13:00
public readonly EntityUid VisualEnt ;
2024-08-09 08:24:05 +02:00
public readonly EntityUid ? Cause ;
2022-04-05 19:22:35 +12:00
/// <summary>
/// Initialize a new instance for processing
/// </summary>
2022-04-01 15:39:26 +13:00
public Explosion ( ExplosionSystem system ,
ExplosionPrototype explosionType ,
2022-04-05 19:22:35 +12:00
ExplosionSpaceTileFlood ? spaceData ,
List < ExplosionGridTileFlood > gridData ,
2022-04-01 15:39:26 +13:00
List < float > tileSetIntensity ,
MapCoordinates epicenter ,
2024-06-02 05:07:41 +01:00
Matrix3x2 spaceMatrix ,
2022-04-01 15:39:26 +13:00
int area ,
2022-04-05 19:22:35 +12:00
float tileBreakScale ,
int maxTileBreak ,
2022-04-09 09:07:02 +12:00
bool canCreateVacuum ,
2022-04-01 15:39:26 +13:00
IEntityManager entMan ,
2022-11-27 23:24:35 +13:00
IMapManager mapMan ,
2024-08-09 08:24:05 +02:00
EntityUid visualEnt ,
2024-09-29 02:27:47 +03:00
EntityUid ? cause ,
SharedMapSystem mapSystem )
2022-04-01 15:39:26 +13:00
{
2022-11-27 23:24:35 +13:00
VisualEnt = visualEnt ;
2024-08-09 08:24:05 +02:00
Cause = cause ;
2022-04-01 15:39:26 +13:00
_system = system ;
2024-09-29 02:27:47 +03:00
_mapSystem = mapSystem ;
2022-04-01 15:39:26 +13:00
ExplosionType = explosionType ;
_tileSetIntensity = tileSetIntensity ;
Epicenter = epicenter ;
Area = area ;
2022-04-05 19:22:35 +12:00
_tileBreakScale = tileBreakScale ;
_maxTileBreak = maxTileBreak ;
2022-04-09 09:07:02 +12:00
_canCreateVacuum = canCreateVacuum ;
2022-04-05 19:22:35 +12:00
_entMan = entMan ;
2022-04-01 15:39:26 +13:00
_xformQuery = entMan . GetEntityQuery < TransformComponent > ( ) ;
_physicsQuery = entMan . GetEntityQuery < PhysicsComponent > ( ) ;
_damageQuery = entMan . GetEntityQuery < DamageableComponent > ( ) ;
2023-05-07 14:57:23 +12:00
_tagQuery = entMan . GetEntityQuery < TagComponent > ( ) ;
_projectileQuery = entMan . GetEntityQuery < ProjectileComponent > ( ) ;
2022-04-01 15:39:26 +13:00
if ( spaceData ! = null )
{
2025-04-17 23:55:09 -04:00
var mapUid = mapSystem . GetMap ( epicenter . MapId ) ;
2022-04-01 15:39:26 +13:00
_explosionData . Add ( new ( )
{
TileLists = spaceData . TileLists ,
2025-05-24 10:55:46 -04:00
Lookup = ( mapUid , entMan . GetComponent < BroadphaseComponent > ( mapUid ) ) ,
2022-04-01 15:39:26 +13:00
MapGrid = null
} ) ;
_spaceMatrix = spaceMatrix ;
2024-06-02 05:07:41 +01:00
Matrix3x2 . Invert ( spaceMatrix , out _invSpaceMatrix ) ;
2022-04-01 15:39:26 +13:00
}
foreach ( var grid in gridData )
{
2023-01-19 03:56:45 +01:00
_explosionData . Add ( new ExplosionData
2022-04-01 15:39:26 +13:00
{
TileLists = grid . TileLists ,
2025-05-24 10:55:46 -04:00
Lookup = ( grid . Grid , entMan . GetComponent < BroadphaseComponent > ( grid . Grid ) ) ,
2023-01-19 03:56:45 +01:00
MapGrid = grid . Grid ,
2022-04-01 15:39:26 +13:00
} ) ;
}
if ( TryGetNextTileEnumerator ( ) )
MoveNext ( ) ;
}
2022-04-05 19:22:35 +12:00
/// <summary>
/// Find the next tile-enumerator. This either means retrieving a set of tiles on the next grid, or incrementing
/// the tile iteration by one and moving back to the first grid. This will also update the current damage, current entity-lookup, etc.
/// </summary>
2022-04-01 15:39:26 +13:00
private bool TryGetNextTileEnumerator ( )
{
while ( CurrentIteration < _tileSetIntensity . Count )
{
_currentIntensity = _tileSetIntensity [ CurrentIteration ] ;
2023-10-15 03:48:25 +11:00
2024-03-14 00:27:08 -04:00
#if DEBUG
2023-10-15 03:48:25 +11:00
if ( _expectedDamage ! = null )
{
// Check that explosion processing hasn't somehow accidentally mutated the damage set.
DebugTools . Assert ( _expectedDamage . Equals ( _currentDamage ) ) ;
_expectedDamage = ExplosionType . DamagePerIntensity * _currentIntensity ;
}
2024-03-14 00:27:08 -04:00
#endif
2023-10-15 03:48:25 +11:00
2022-04-01 15:39:26 +13:00
_currentDamage = ExplosionType . DamagePerIntensity * _currentIntensity ;
// only throw if either the explosion is small, or if this is the outer ring of a large explosion.
var doThrow = Area < _system . ThrowLimit | | CurrentIteration > _tileSetIntensity . Count - 6 ;
_currentThrowForce = doThrow ? 10 * MathF . Sqrt ( _currentIntensity ) : 0 ;
// for each grid/space tile set
while ( _currentDataIndex < _explosionData . Count )
{
// try get any tile hash-set corresponding to this intensity
var tileSets = _explosionData [ _currentDataIndex ] . TileLists ;
if ( ! tileSets . TryGetValue ( CurrentIteration , out var tileList ) )
{
_currentDataIndex + + ;
continue ;
}
_currentEnumerator = tileList . GetEnumerator ( ) ;
_currentLookup = _explosionData [ _currentDataIndex ] . Lookup ;
_currentGrid = _explosionData [ _currentDataIndex ] . MapGrid ;
_currentDataIndex + + ;
2022-04-05 19:22:35 +12:00
// sanity checks, in case something changed while the explosion was being processed over several ticks.
2025-05-24 10:55:46 -04:00
if ( _currentLookup . Comp . Deleted | | _currentGrid ! = null & & ! _entMan . EntityExists ( _currentGrid . Value ) )
2022-04-05 19:22:35 +12:00
continue ;
2022-04-01 15:39:26 +13:00
return true ;
}
2022-04-05 19:22:35 +12:00
// All the tiles belonging to this explosion iteration have been processed. Move onto the next iteration and
// reset the grid counter.
2022-04-01 15:39:26 +13:00
CurrentIteration + + ;
_currentDataIndex = 0 ;
}
2022-04-05 19:22:35 +12:00
// No more explosion tiles to process
2022-04-01 15:39:26 +13:00
FinishedProcessing = true ;
return false ;
}
2022-04-05 19:22:35 +12:00
/// <summary>
/// Get the next tile that needs processing
/// </summary>
2022-04-01 15:39:26 +13:00
private bool MoveNext ( )
{
if ( FinishedProcessing )
return false ;
while ( ! FinishedProcessing )
{
if ( _currentEnumerator . MoveNext ( ) )
return true ;
else
TryGetNextTileEnumerator ( ) ;
}
return false ;
}
2022-04-05 19:22:35 +12:00
/// <summary>
2022-04-06 19:35:18 +10:00
/// Attempt to process (i.e., damage entities) some number of grid tiles.
2022-04-05 19:22:35 +12:00
/// </summary>
2022-04-01 15:39:26 +13:00
public int Process ( int processingTarget )
{
// In case the explosion terminated early last tick due to exceeding the allocated processing time, use this
// time to update the tiles.
SetTiles ( ) ;
int processed ;
for ( processed = 0 ; processed < processingTarget ; processed + + )
{
if ( processed % ExplosionSystem . TileCheckIteration = = 0 & &
_system . Stopwatch . Elapsed . TotalMilliseconds > _system . MaxProcessingTime )
{
break ;
}
2022-04-05 19:22:35 +12:00
// Is the current tile on a grid (instead of in space)?
2025-05-24 10:55:46 -04:00
if ( _currentGrid is { } currentGrid & &
_mapSystem . TryGetTileRef ( currentGrid , currentGrid . Comp , _currentEnumerator . Current , out var tileRef ) & &
2022-04-01 15:39:26 +13:00
! tileRef . Tile . IsEmpty )
{
2025-05-24 10:55:46 -04:00
if ( ! _tileUpdateDict . TryGetValue ( currentGrid , out var tileUpdateList ) )
2022-04-01 15:39:26 +13:00
{
tileUpdateList = new ( ) ;
2025-05-24 10:55:46 -04:00
_tileUpdateDict [ currentGrid ] = tileUpdateList ;
2022-04-01 15:39:26 +13:00
}
2022-04-05 19:22:35 +12:00
// damage entities on the tile. Also figures out whether there are any solid entities blocking the floor
// from being destroyed.
2022-04-01 15:39:26 +13:00
var canDamageFloor = _system . ExplodeTile ( _currentLookup ,
2025-05-24 10:55:46 -04:00
currentGrid ,
2022-04-01 15:39:26 +13:00
_currentEnumerator . Current ,
_currentThrowForce ,
_currentDamage ,
Epicenter ,
ProcessedEntities ,
2024-03-14 00:27:08 -04:00
ExplosionType . ID ,
2024-08-09 08:24:05 +02:00
ExplosionType . FireStacks ,
Cause ) ;
2022-04-01 15:39:26 +13:00
2022-04-05 19:22:35 +12:00
// If the floor is not blocked by some dense object, damage the floor tiles.
2022-04-01 15:39:26 +13:00
if ( canDamageFloor )
2022-04-09 09:07:02 +12:00
_system . DamageFloorTile ( tileRef , _currentIntensity * _tileBreakScale , _maxTileBreak , _canCreateVacuum , tileUpdateList , ExplosionType ) ;
2022-04-01 15:39:26 +13:00
}
else
{
2022-04-05 19:22:35 +12:00
// The current "tile" is in space. Damage any entities in that region
2022-04-01 15:39:26 +13:00
_system . ExplodeSpace ( _currentLookup ,
_spaceMatrix ,
_invSpaceMatrix ,
_currentEnumerator . Current ,
_currentThrowForce ,
_currentDamage ,
Epicenter ,
ProcessedEntities ,
2024-03-14 00:27:08 -04:00
ExplosionType . ID ,
2024-08-09 08:24:05 +02:00
ExplosionType . FireStacks ,
Cause ) ;
2022-04-01 15:39:26 +13:00
}
if ( ! MoveNext ( ) )
break ;
}
2022-04-05 19:22:35 +12:00
// Update damaged/broken tiles on the grid.
2022-04-01 15:39:26 +13:00
SetTiles ( ) ;
return processed ;
}
private void SetTiles ( )
{
2022-04-05 19:22:35 +12:00
// Updating the grid can result in chunk collision regeneration & slow processing by the shuttle system.
// Therefore, tile breaking may be configure to only happen at the end of an explosion, rather than during every
// tick.
2022-04-01 15:39:26 +13:00
if ( ! _system . IncrementalTileBreaking & & ! FinishedProcessing )
return ;
foreach ( var ( grid , list ) in _tileUpdateDict )
{
2022-12-12 14:59:02 +11:00
if ( list . Count > 0 & & _entMan . EntityExists ( grid . Owner ) )
2022-04-01 15:39:26 +13:00
{
2024-09-29 02:27:47 +03:00
_mapSystem . SetTiles ( grid . Owner , grid , list ) ;
2022-04-01 15:39:26 +13:00
}
}
_tileUpdateDict . Clear ( ) ;
}
}
2024-03-29 23:46:05 +00:00
/// <summary>
/// Data needed to spawn an explosion with <see cref="ExplosionSystem.SpawnExplosion"/>.
/// </summary>
2025-06-28 15:34:15 -04:00
public sealed class QueuedExplosion ( ExplosionPrototype proto )
2024-03-29 23:46:05 +00:00
{
public MapCoordinates Epicenter ;
2025-06-28 15:34:15 -04:00
public ExplosionPrototype Proto = proto ;
2024-03-29 23:46:05 +00:00
public float TotalIntensity , Slope , MaxTileIntensity , TileBreakScale ;
public int MaxTileBreak ;
public bool CanCreateVacuum ;
2024-08-09 08:24:05 +02:00
public EntityUid ? Cause ; // The entity that exploded, for logging purposes.
2024-03-29 23:46:05 +00:00
}