diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
index 91cbba648c..617bc55276 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
@@ -1,16 +1,11 @@
using System.Linq;
using System.Numerics;
-using System.Reflection;
-using Content.Server.Explosion.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
-using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Maps;
-using Content.Shared.Mind.Components;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
-using Robust.Shared.Spawners;
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -55,6 +50,13 @@ public sealed partial class ExplosionSystem
///
private int _previousTileIteration;
+ ///
+ /// This list is used when raising to avoid allocating a new list per event.
+ ///
+ private readonly List _containedEntities = new();
+
+ private readonly List<(EntityUid, DamageSpecifier)> _toDamage = new();
+
private List _anchored = new();
private void OnMapChanged(MapChangedEvent ev)
@@ -84,8 +86,6 @@ public sealed partial class ExplosionSystem
Stopwatch.Restart();
var x = Stopwatch.Elapsed.TotalMilliseconds;
- var availableTime = MaxProcessingTime;
-
var tilesRemaining = TilesPerTick;
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
{
@@ -369,64 +369,73 @@ public sealed partial class ExplosionSystem
return SpaceQueryCallback(ref state, in uid);
}
+ private DamageSpecifier GetDamage(EntityUid uid,
+ string id, DamageSpecifier damage)
+ {
+ // 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();
+ _toDamage.Add((uid, GetDamage(uid, prototype, originalDamage)));
+
+ 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);
+
+ if (_containedEntities.Count == 0)
+ continue;
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (ev.DamageCoefficient != 1)
+ damage *= ev.DamageCoefficient;
+
+ _toDamage.EnsureCapacity(_toDamage.Count + _containedEntities.Count);
+ foreach (var contained in _containedEntities)
+ {
+ var newDamage = GetDamage(contained, prototype, damage);
+ _toDamage.Add((contained, newDamage));
+ }
+ }
+ }
+
///
/// This function actually applies the explosion affects to an entity.
///
private void ProcessEntity(
EntityUid uid,
MapCoordinates epicenter,
- DamageSpecifier? damage,
+ DamageSpecifier? originalDamage,
float throwForce,
string id,
TransformComponent? xform)
{
- // damage
- if (damage != null && _damageQuery.TryGetComponent(uid, out var damageable))
+ if (originalDamage != null)
{
- // 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);
-
- ev.DamageCoefficient = Math.Max(0, ev.DamageCoefficient);
-
- // 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))
+ GetEntitiesToDamage(uid, originalDamage, id);
+ foreach (var (entity, damage) in _toDamage)
{
- _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);
- }
-
- // if it's a container, try to damage all its contents
- if (_containersQuery.TryGetComponent(uid, out var containers))
- {
- foreach (var container in containers.Containers.Values)
- {
- foreach (var ent in container.ContainedEntities)
- {
- // setting throw force to 0 to prevent offset items inside containers
- ProcessEntity(ent, epicenter, damage, 0f, id, _transformQuery.GetComponent(uid));
- }
+ // TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
+ _damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
}
}
// throw
- if (xform != null // null implies anchored
+ if (xform != null // null implies anchored or in a container
&& !xform.Anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
@@ -442,10 +451,6 @@ public sealed partial class ExplosionSystem
_projectileQuery,
throwForce);
}
-
- // TODO EXPLOSION puddle / flammable ignite?
-
- // TODO EXPLOSION deaf/ear damage? other explosion effects?
}
///
@@ -845,4 +850,3 @@ sealed class Explosion
_tileUpdateDict.Clear();
}
}
-
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
index fb8494c680..be62aeb5ed 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
@@ -21,7 +21,6 @@ using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
-using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
@@ -53,11 +52,9 @@ public sealed partial class ExplosionSystem : EntitySystem
[Dependency] private readonly SharedMapSystem _map = default!;
private EntityQuery _transformQuery;
- private EntityQuery _containersQuery;
private EntityQuery _damageQuery;
private EntityQuery _physicsQuery;
private EntityQuery _projectileQuery;
- private EntityQuery _mindQuery;
///
/// "Tile-size" for space when there are no nearby grids to use as a reference.
@@ -107,11 +104,9 @@ public sealed partial class ExplosionSystem : EntitySystem
InitVisuals();
_transformQuery = GetEntityQuery();
- _containersQuery = GetEntityQuery();
_damageQuery = GetEntityQuery();
_physicsQuery = GetEntityQuery();
_projectileQuery = GetEntityQuery();
- _mindQuery = GetEntityQuery();
}
private void OnReset(RoundRestartCleanupEvent ev)
diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs
index e3e6699537..5ceb4a8d60 100644
--- a/Content.Server/Hands/Systems/HandsSystem.cs
+++ b/Content.Server/Hands/Systems/HandsSystem.cs
@@ -8,6 +8,7 @@ using Content.Server.Stunnable;
using Content.Shared.ActionBlocker;
using Content.Shared.Body.Part;
using Content.Shared.CombatMode;
+using Content.Shared.Explosion;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
@@ -54,6 +55,8 @@ namespace Content.Server.Hands.Systems
SubscribeLocalEvent(GetComponentState);
+ SubscribeLocalEvent(OnExploded);
+
CommandBinds.Builder
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
.Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack))
@@ -73,6 +76,15 @@ namespace Content.Server.Hands.Systems
args.State = new HandsComponentState(hands);
}
+ private void OnExploded(Entity ent, ref BeforeExplodeEvent args)
+ {
+ foreach (var hand in ent.Comp.Hands.Values)
+ {
+ if (hand.HeldEntity is {} uid)
+ args.Contents.Add(uid);
+ }
+ }
+
private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent args)
{
if (args.Handled)
diff --git a/Content.Server/Inventory/ServerInventorySystem.cs b/Content.Server/Inventory/ServerInventorySystem.cs
index f80a604ad5..f8d4bd3a1f 100644
--- a/Content.Server/Inventory/ServerInventorySystem.cs
+++ b/Content.Server/Inventory/ServerInventorySystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Storage.EntitySystems;
using Content.Shared.Clothing.Components;
+using Content.Shared.Explosion;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@@ -15,11 +16,26 @@ namespace Content.Server.Inventory
{
base.Initialize();
+ SubscribeLocalEvent(OnExploded);
+
SubscribeLocalEvent(OnUseInHand);
SubscribeNetworkEvent(OnOpenSlotStorage);
}
+ private void OnExploded(Entity ent, ref BeforeExplodeEvent args)
+ {
+ if (!TryGetContainerSlotEnumerator(ent, out var slots, ent.Comp))
+ return;
+
+ // explode each item in their inventory too
+ while (slots.MoveNext(out var slot))
+ {
+ if (slot.ContainedEntity != null)
+ args.Contents.Add(slot.ContainedEntity.Value);
+ }
+ }
+
private void OnUseInHand(EntityUid uid, ClothingComponent component, UseInHandEvent args)
{
if (args.Handled || !component.QuickEquip)
diff --git a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs
index 4bcad622c7..efb3734962 100644
--- a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs
+++ b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs
@@ -4,6 +4,7 @@ using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.Storage.Components;
using Content.Shared.Destructible;
+using Content.Shared.Explosion;
using Content.Shared.Foldable;
using Content.Shared.Interaction;
using Content.Shared.Lock;
@@ -46,6 +47,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnWeldableAttempt);
+ SubscribeLocalEvent(OnExploded);
SubscribeLocalEvent(OnInsideInhale);
SubscribeLocalEvent(OnInsideExhale);
@@ -98,6 +100,15 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
}
}
+ private void OnExploded(Entity ent, ref BeforeExplodeEvent args)
+ {
+ if (ent.Comp.ExplosionDamageCoefficient <= 0)
+ return;
+
+ args.Contents.AddRange(ent.Comp.Contents.ContainedEntities);
+ args.DamageCoefficient *= ent.Comp.ExplosionDamageCoefficient;
+ }
+
protected override void TakeGas(EntityUid uid, SharedEntityStorageComponent component)
{
if (!component.Airtight)
diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs
index d59006e753..a38577edfa 100644
--- a/Content.Server/Storage/EntitySystems/StorageSystem.cs
+++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
+using Content.Shared.Explosion;
using Content.Shared.Ghost;
using Content.Shared.Hands;
using Content.Shared.Lock;
@@ -27,6 +28,7 @@ public sealed partial class StorageSystem : SharedStorageSystem
base.Initialize();
SubscribeLocalEvent>(AddUiVerb);
SubscribeLocalEvent(OnBoundUIClosed);
+ SubscribeLocalEvent(OnExploded);
SubscribeLocalEvent(OnStorageFillMapInit);
}
@@ -97,6 +99,11 @@ public sealed partial class StorageSystem : SharedStorageSystem
}
}
+ private void OnExploded(Entity ent, ref BeforeExplodeEvent args)
+ {
+ args.Contents.AddRange(ent.Comp.Container.ContainedEntities);
+ }
+
///
/// Opens the storage UI for an entity
///
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs
index b36133276c..9337e79439 100644
--- a/Content.Shared/Damage/Systems/DamageableSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageableSystem.cs
@@ -1,7 +1,9 @@
using System.Linq;
+using Content.Shared.Administration.Logs;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
+using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Radiation.Events;
@@ -16,12 +18,14 @@ namespace Content.Shared.Damage
public sealed class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
private EntityQuery _appearanceQuery;
private EntityQuery _damageableQuery;
+ private EntityQuery _mindContainerQuery;
public override void Initialize()
{
@@ -33,6 +37,7 @@ namespace Content.Shared.Damage
_appearanceQuery = GetEntityQuery();
_damageableQuery = GetEntityQuery();
+ _mindContainerQuery = GetEntityQuery();
}
///
diff --git a/Content.Shared/Explosion/ExplosionEvents.cs b/Content.Shared/Explosion/ExplosionEvents.cs
index 37c956e165..7b0cde48e6 100644
--- a/Content.Shared/Explosion/ExplosionEvents.cs
+++ b/Content.Shared/Explosion/ExplosionEvents.cs
@@ -1,6 +1,5 @@
+using Content.Shared.Damage;
using Content.Shared.Inventory;
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
namespace Content.Shared.Explosion;
@@ -20,3 +19,34 @@ public record struct GetExplosionResistanceEvent(string ExplosionPrototype) : II
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
}
+
+///
+/// This event is raised directed at an entity that is about to receive damage from an explosion. It can be used to
+/// recursively add contained/child entities that should also receive damage. E.g., entities in a player's inventory
+/// or backpack. This event will be raised recursively so a matchbox in a backpack in a player's inventory
+/// will also receive this event.
+///
+[ByRefEvent]
+public record struct BeforeExplodeEvent(DamageSpecifier Damage, string Id, List Contents)
+{
+ ///
+ /// The damage that will be received by this entity. Note that the entity's explosion resistance has already been
+ /// used to modify this damage.
+ ///
+ public readonly DamageSpecifier Damage = Damage;
+
+ ///
+ /// ID of the explosion prototype.
+ ///
+ public readonly string Id = Id;
+
+ ///
+ /// Damage multiplier for modifying the damage that will get dealt to contained entities.
+ ///
+ public float DamageCoefficient = 1;
+
+ ///
+ /// Contained/child entities that should receive recursive explosion damage.
+ ///
+ public readonly List Contents = Contents;
+}
diff --git a/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs b/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs
index b4cd18f4d5..e70c59c9d6 100644
--- a/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs
+++ b/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs
@@ -124,6 +124,12 @@ public abstract partial class SharedEntityStorageComponent : Component
///
[ViewVariables]
public Container Contents = default!;
+
+ ///
+ /// Multiplier for explosion damage that gets applied to contained entities.
+ ///
+ [DataField]
+ public float ExplosionDamageCoefficient = 1;
}
[Serializable, NetSerializable]
diff --git a/Resources/Prototypes/Body/Parts/slime.yml b/Resources/Prototypes/Body/Parts/slime.yml
index c11d723950..4b0e94b008 100644
--- a/Resources/Prototypes/Body/Parts/slime.yml
+++ b/Resources/Prototypes/Body/Parts/slime.yml
@@ -1,7 +1,7 @@
# TODO BODY: Part damage
- type: entity
id: PartSlime
- parent: [BaseItem, PartBase]
+ parent: [BaseItem, BasePart]
name: "slime body part"
abstract: true