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;
/// </summary>
private int _previousTileIteration;
+ /// <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();
+
private List<EntityUid> _anchored = new();
private void OnMapChanged(MapChangedEvent ev)
Stopwatch.Restart();
var x = Stopwatch.Elapsed.TotalMilliseconds;
- var availableTime = MaxProcessingTime;
-
var tilesRemaining = TilesPerTick;
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
{
return SpaceQueryCallback(ref state, in uid);
}
- /// <summary>
- /// This function actually applies the explosion affects to an entity.
- /// </summary>
- private void ProcessEntity(
- EntityUid uid,
- MapCoordinates epicenter,
- DamageSpecifier? damage,
- float throwForce,
- string id,
- TransformComponent? xform)
+ private DamageSpecifier GetDamage(EntityUid uid,
+ string id, DamageSpecifier damage)
{
- // damage
- 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);
+ // 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)));
- ev.DamageCoefficient = Math.Max(0, ev.DamageCoefficient);
+ 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);
- // TODO explosion entity
- // Move explosion data into the existing explosion visuals entity
- // Give each explosion a unique name, include in admin logs.
+ if (_containedEntities.Count == 0)
+ continue;
- // 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))
+ _toDamage.EnsureCapacity(_toDamage.Count + _containedEntities.Count);
+ foreach (var contained in _containedEntities)
{
- _adminLogger.Add(LogType.Explosion, LogImpact.Medium,
- $"Explosion caused [{damage.Total}] damage to {ToPrettyString(uid):target} at {xform?.Coordinates}");
+ var newDamage = GetDamage(contained, prototype, damage);
+ _toDamage.Add((contained, newDamage));
}
-
- _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))
+ /// <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,
+ TransformComponent? xform)
+ {
+ if (originalDamage != null)
{
- foreach (var container in containers.Containers.Values)
+ GetEntitiesToDamage(uid, originalDamage, id);
+ foreach (var (entity, damage) in _toDamage)
{
- 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)
_projectileQuery,
throwForce);
}
-
- // TODO EXPLOSION puddle / flammable ignite?
-
- // TODO EXPLOSION deaf/ear damage? other explosion effects?
}
/// <summary>
_tileUpdateDict.Clear();
}
}
-
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;
[Dependency] private readonly SharedMapSystem _map = default!;
private EntityQuery<TransformComponent> _transformQuery;
- private EntityQuery<ContainerManagerComponent> _containersQuery;
private EntityQuery<DamageableComponent> _damageQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<ProjectileComponent> _projectileQuery;
- private EntityQuery<MindComponent> _mindQuery;
/// <summary>
/// "Tile-size" for space when there are no nearby grids to use as a reference.
InitVisuals();
_transformQuery = GetEntityQuery<TransformComponent>();
- _containersQuery = GetEntityQuery<ContainerManagerComponent>();
_damageQuery = GetEntityQuery<DamageableComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_projectileQuery = GetEntityQuery<ProjectileComponent>();
- _mindQuery = GetEntityQuery<MindComponent>();
}
private void OnReset(RoundRestartCleanupEvent ev)
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;
SubscribeLocalEvent<HandsComponent, ComponentGetState>(GetComponentState);
+ SubscribeLocalEvent<HandsComponent, BeforeExplodeEvent>(OnExploded);
+
CommandBinds.Builder
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
.Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack))
args.State = new HandsComponentState(hands);
}
+ private void OnExploded(Entity<HandsComponent> 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)
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;
{
base.Initialize();
+ SubscribeLocalEvent<InventoryComponent, BeforeExplodeEvent>(OnExploded);
+
SubscribeLocalEvent<ClothingComponent, UseInHandEvent>(OnUseInHand);
SubscribeNetworkEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
}
+ private void OnExploded(Entity<InventoryComponent> 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)
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;
SubscribeLocalEvent<EntityStorageComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<EntityStorageComponent, WeldableAttemptEvent>(OnWeldableAttempt);
+ SubscribeLocalEvent<EntityStorageComponent, BeforeExplodeEvent>(OnExploded);
SubscribeLocalEvent<InsideEntityStorageComponent, InhaleLocationEvent>(OnInsideInhale);
SubscribeLocalEvent<InsideEntityStorageComponent, ExhaleLocationEvent>(OnInsideExhale);
}
}
+ private void OnExploded(Entity<EntityStorageComponent> 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)
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;
base.Initialize();
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
SubscribeLocalEvent<StorageComponent, BoundUIClosedEvent>(OnBoundUIClosed);
+ SubscribeLocalEvent<StorageComponent, BeforeExplodeEvent>(OnExploded);
SubscribeLocalEvent<StorageFillComponent, MapInitEvent>(OnStorageFillMapInit);
}
}
}
+ private void OnExploded(Entity<StorageComponent> ent, ref BeforeExplodeEvent args)
+ {
+ args.Contents.AddRange(ent.Comp.Container.ContainedEntities);
+ }
+
/// <summary>
/// Opens the storage UI for an entity
/// </summary>
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;
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<AppearanceComponent> _appearanceQuery;
private EntityQuery<DamageableComponent> _damageableQuery;
+ private EntityQuery<MindContainerComponent> _mindContainerQuery;
public override void Initialize()
{
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_damageableQuery = GetEntityQuery<DamageableComponent>();
+ _mindContainerQuery = GetEntityQuery<MindContainerComponent>();
}
/// <summary>
+using Content.Shared.Damage;
using Content.Shared.Inventory;
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
namespace Content.Shared.Explosion;
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
}
+
+/// <summary>
+/// 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.
+/// </summary>
+[ByRefEvent]
+public record struct BeforeExplodeEvent(DamageSpecifier Damage, string Id, List<EntityUid> Contents)
+{
+ /// <summary>
+ /// The damage that will be received by this entity. Note that the entity's explosion resistance has already been
+ /// used to modify this damage.
+ /// </summary>
+ public readonly DamageSpecifier Damage = Damage;
+
+ /// <summary>
+ /// ID of the explosion prototype.
+ /// </summary>
+ public readonly string Id = Id;
+
+ /// <summary>
+ /// Damage multiplier for modifying the damage that will get dealt to contained entities.
+ /// </summary>
+ public float DamageCoefficient = 1;
+
+ /// <summary>
+ /// Contained/child entities that should receive recursive explosion damage.
+ /// </summary>
+ public readonly List<EntityUid> Contents = Contents;
+}
/// </summary>
[ViewVariables]
public Container Contents = default!;
+
+ /// <summary>
+ /// Multiplier for explosion damage that gets applied to contained entities.
+ /// </summary>
+ [DataField]
+ public float ExplosionDamageCoefficient = 1;
}
[Serializable, NetSerializable]
# TODO BODY: Part damage
- type: entity
id: PartSlime
- parent: [BaseItem, PartBase]
+ parent: [BaseItem, BasePart]
name: "slime body part"
abstract: true