using System.Linq;
using System.Numerics;
+using System.Reflection;
using Content.Server.Explosion.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
-public sealed partial class ExplosionSystem : EntitySystem
+public sealed partial class ExplosionSystem
{
/// <summary>
/// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>.
/// 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>
- public bool IsBlockingTurf(EntityUid uid, EntityQuery<PhysicsComponent> physicsQuery)
+ public bool IsBlockingTurf(EntityUid uid)
{
if (EntityManager.IsQueuedForDeletion(uid))
return false;
- if (!physicsQuery.TryGetComponent(uid, out var physics))
+ if (!_physicsQuery.TryGetComponent(uid, out var physics))
return false;
return physics.CanCollide && physics.Hard && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0;
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet<EntityUid> processed,
- string id,
- EntityQuery<TransformComponent> xformQuery,
- EntityQuery<DamageableComponent> damageQuery,
- EntityQuery<PhysicsComponent> physicsQuery,
- EntityQuery<TagComponent> tagQuery,
- EntityQuery<ProjectileComponent> projectileQuery)
+ string id)
{
var gridBox = new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize);
// get the entities on a tile. Note that we cannot process them directly, or we get
// enumerator-changed-while-enumerating errors.
- List<TransformComponent> list = new();
- var state = (list, processed, xformQuery);
+ List<(EntityUid, TransformComponent)> list = new();
+ var state = (list, processed, _transformQuery);
// get entities:
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.StaticSundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
// process those entities
- foreach (var xform in list)
+ foreach (var (uid, xform) in list)
{
- ProcessEntity(xform.Owner, epicenter, damage, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
+ ProcessEntity(uid, epicenter, damage, throwForce, id, xform);
}
// process anchored entities
foreach (var entity in anchoredList)
{
processed.Add(entity);
- ProcessEntity(entity, epicenter, damage, throwForce, id, null, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
+ ProcessEntity(entity, epicenter, damage, throwForce, id, null);
}
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
{
foreach (var entity in grid.GetAnchoredEntities(tile))
{
- tileBlocked |= IsBlockingTurf(entity, physicsQuery);
+ tileBlocked |= IsBlockingTurf(entity);
}
}
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
- foreach (var xform in list)
+ foreach (var (uid, xform) in list)
{
// 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.
- ProcessEntity(xform.Owner, epicenter, null, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
+ ProcessEntity(uid, epicenter, null, throwForce, id, xform);
}
return !tileBlocked;
}
private bool GridQueryCallback(
- ref (List<TransformComponent> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
+ ref (List<(EntityUid, TransformComponent)> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
in EntityUid uid)
{
if (state.Processed.Add(uid) && state.XformQuery.TryGetComponent(uid, out var xform))
- state.List.Add(xform);
+ state.List.Add((uid, xform));
return true;
}
private bool GridQueryCallback(
- ref (List<TransformComponent> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
+ ref (List<(EntityUid, TransformComponent)> List, HashSet<EntityUid> Processed, EntityQuery<TransformComponent> XformQuery) state,
in FixtureProxy proxy)
{
var owner = proxy.Entity;
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet<EntityUid> processed,
- string id,
- EntityQuery<TransformComponent> xformQuery,
- EntityQuery<DamageableComponent> damageQuery,
- EntityQuery<PhysicsComponent> physicsQuery,
- EntityQuery<TagComponent> tagQuery,
- EntityQuery<ProjectileComponent> projectileQuery)
+ string id)
{
var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize));
var worldBox = spaceMatrix.TransformBox(gridBox);
- var list = new List<TransformComponent>();
- var state = (list, processed, invSpaceMatrix, lookup.Owner, xformQuery, gridBox);
+ var list = new List<(EntityUid, TransformComponent)>();
+ var state = (list, processed, invSpaceMatrix, lookup.Owner, _transformQuery, gridBox);
// get entities:
lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.StaticSundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
- foreach (var xform in state.Item1)
+ foreach (var (uid, xform) in state.Item1)
{
- processed.Add(xform.Owner);
- ProcessEntity(xform.Owner, epicenter, damage, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
+ processed.Add(uid);
+ ProcessEntity(uid, epicenter, damage, throwForce, id, xform);
}
if (throwForce <= 0)
lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
- foreach (var xform in list)
+ foreach (var (uid, xform) in list)
{
- ProcessEntity(xform.Owner, epicenter, null, throwForce, id, xform, damageQuery, physicsQuery, xformQuery, tagQuery, projectileQuery);
+ ProcessEntity(uid, epicenter, null, throwForce, id, xform);
}
}
private bool SpaceQueryCallback(
- ref (List<TransformComponent> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
+ ref (List<(EntityUid, TransformComponent)> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
in EntityUid uid)
{
if (state.Processed.Contains(uid))
{
// parented directly to the map, use local position
if (state.GridBox.Contains(state.InvSpaceMatrix.Transform(xform.LocalPosition)))
- state.List.Add(xform);
+ state.List.Add((uid, xform));
return true;
}
// finally check if it intersects our tile
if (state.GridBox.Contains(state.InvSpaceMatrix.Transform(_transformSystem.GetWorldPosition(xform, state.XformQuery))))
- state.List.Add(xform);
+ state.List.Add((uid, xform));
return true;
}
private bool SpaceQueryCallback(
- ref (List<TransformComponent> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
+ ref (List<(EntityUid, TransformComponent)> List, HashSet<EntityUid> Processed, Matrix3 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery<TransformComponent> XformQuery, Box2 GridBox) state,
in FixtureProxy proxy)
{
var uid = proxy.Entity;
DamageSpecifier? damage,
float throwForce,
string id,
- TransformComponent? xform,
- EntityQuery<DamageableComponent> damageQuery,
- EntityQuery<PhysicsComponent> physicsQuery,
- EntityQuery<TransformComponent> transformQuery,
- EntityQuery<TagComponent> tagQuery,
- EntityQuery<ProjectileComponent> projectileQuery)
+ TransformComponent? xform)
{
// damage
- if (damage != null && damageQuery.TryGetComponent(uid, out var damageable))
+ if (damage != null && _damageQuery.TryGetComponent(uid, out var damageable))
{
+ // TODO Explosion Performance
+ // Cache this? I.e., instead of raising an event, check for a component?
var ev = new GetExplosionResistanceEvent(id);
- RaiseLocalEvent(uid, ref ev, false);
+ RaiseLocalEvent(uid, ref ev);
ev.DamageCoefficient = Math.Max(0, ev.DamageCoefficient);
- //todo need a way to track origin of explosion
- if (ev.DamageCoefficient == 1)
+ // TODO explosion entity
+ // Move explosion data into the existing explosion visuals entity
+ // Give each explosion a unique name, include in admin logs.
+
+ // TODO Explosion Performance
+ // This creates a new dictionary. Maybe we should just re-use a private local damage specifier and update it.
+ // Though most entities shouldn't have explosion resistance, so maybe its fine.
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (ev.DamageCoefficient != 1)
+ damage *= ev.DamageCoefficient;
+
+ // Log damage to players. Damage is logged before dealing damage so that the position can be logged before
+ // the entity gets deleted.
+ if (_mindQuery.HasComponent(uid))
{
- // no damage-dict multiplication required.
- _damageableSystem.TryChangeDamage(uid, damage, ignoreResistances: true, damageable: damageable);
- if (HasComp<MindContainerComponent>(uid) || HasComp<ExplosiveComponent>(uid))
- {
- var damageStr = string.Join(", ", damage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}"));
- _adminLogger.Add(LogType.Explosion, LogImpact.Medium,
- $"Explosion caused [{damageStr}] to {ToPrettyString(uid):target} at {Transform(uid).Coordinates}");
- }
- }
- else
- {
- var appliedDamage = damage * ev.DamageCoefficient;
- _damageableSystem.TryChangeDamage(uid, appliedDamage, ignoreResistances: true, damageable: damageable);
- if (HasComp<MindContainerComponent>(uid) || HasComp<ExplosiveComponent>(uid))
- {
- var damageStr = string.Join(", ", appliedDamage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}"));
- _adminLogger.Add(LogType.Explosion, LogImpact.Medium,
- $"Explosion caused [{damageStr}] to {ToPrettyString(uid):target} at {Transform(uid).Coordinates}");
- }
+ _adminLogger.Add(LogType.Explosion, LogImpact.Medium,
+ $"Explosion caused [{damage.Total}] damage to {ToPrettyString(uid):target} at {xform?.Coordinates}");
}
+
+ _damageableSystem.TryChangeDamage(uid, damage, ignoreResistances: true, damageable: damageable);
}
// throw
&& !xform.Anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
- && physicsQuery.TryGetComponent(uid, out var physics)
+ && _physicsQuery.TryGetComponent(uid, out var physics)
&& physics.BodyType == BodyType.Dynamic)
{
- var pos = _transformSystem.GetWorldPosition(xform, transformQuery);
+ var pos = _transformSystem.GetWorldPosition(xform);
_throwingSystem.TryThrow(
uid,
pos - epicenter.Position,
physics,
xform,
- projectileQuery,
+ _projectileQuery,
throwForce);
}
// Variables used for enumerating over tiles, grids, etc
private DamageSpecifier _currentDamage = default!;
+#if DEBUG
+ private DamageSpecifier? _expectedDamage;
+#endif
private BroadphaseComponent _currentLookup = default!;
private MapGridComponent? _currentGrid;
private float _currentIntensity;
while (CurrentIteration < _tileSetIntensity.Count)
{
_currentIntensity = _tileSetIntensity[CurrentIteration];
+
+ #if DEBUG
+ if (_expectedDamage != null)
+ {
+ // Check that explosion processing hasn't somehow accidentally mutated the damage set.
+ DebugTools.Assert(_expectedDamage.Equals(_currentDamage));
+ _expectedDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
+ }
+ #endif
+
_currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
// only throw if either the explosion is small, or if this is the outer ring of a large explosion.
_currentDamage,
Epicenter,
ProcessedEntities,
- ExplosionType.ID,
- _xformQuery,
- _damageQuery,
- _physicsQuery,
- _tagQuery,
- _projectileQuery);
+ ExplosionType.ID);
// If the floor is not blocked by some dense object, damage the floor tiles.
if (canDamageFloor)
_currentDamage,
Epicenter,
ProcessedEntities,
- ExplosionType.ID,
- _xformQuery,
- _damageQuery,
- _physicsQuery,
- _tagQuery,
- _projectileQuery);
+ ExplosionType.ID);
}
if (!MoveNext())