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