Fire extinguishers can now extinguish items, including when held/worn (#36267)

* Fire extinguishers now put out candles

This did not actually require any changes to flammable or extinguishers directly, the only necessary changes were to make the collision actually work.

Vapor entities (also used for fire extinguishers) now have a collision layer, so they can hit items.

Added a new FlammableSetCollisionWake component to actually enable collision on candles while they are lit, because otherwise CollisionWake on entities gets in the way too.

* Extinguishing items is now relayed to held/worn items

This means held candles get extinguished too.

Involved moving the core logic of ExtinguishReaction into an event so that it can be relayed via the existing hand/inventory relay logic.

* Add helper functions for subscribing to relayed events.

Use these in FlammableSystem

* Make extinguishers work on cigarettes too

A bunch of renaming to make the rest of my code work with SmokableComponent

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
Pieter-Jan Briers
2025-04-14 11:00:47 +02:00
committed by GitHub
parent d0b4f4744c
commit 843d79be5f
12 changed files with 275 additions and 9 deletions

View File

@@ -23,6 +23,7 @@ using Content.Shared.Timing;
using Content.Shared.Toggleable; using Content.Shared.Toggleable;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Robust.Server.Audio; using Robust.Server.Audio;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
@@ -73,6 +74,7 @@ namespace Content.Server.Atmos.EntitySystems
SubscribeLocalEvent<FlammableComponent, TileFireEvent>(OnTileFire); SubscribeLocalEvent<FlammableComponent, TileFireEvent>(OnTileFire);
SubscribeLocalEvent<FlammableComponent, RejuvenateEvent>(OnRejuvenate); SubscribeLocalEvent<FlammableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<FlammableComponent, ResistFireAlertEvent>(OnResistFireAlert); SubscribeLocalEvent<FlammableComponent, ResistFireAlertEvent>(OnResistFireAlert);
Subs.SubscribeWithRelay<FlammableComponent, ExtinguishEvent>(OnExtinguishEvent);
SubscribeLocalEvent<IgniteOnCollideComponent, StartCollideEvent>(IgniteOnCollide); SubscribeLocalEvent<IgniteOnCollideComponent, StartCollideEvent>(IgniteOnCollide);
SubscribeLocalEvent<IgniteOnCollideComponent, LandEvent>(OnIgniteLand); SubscribeLocalEvent<IgniteOnCollideComponent, LandEvent>(OnIgniteLand);
@@ -84,6 +86,14 @@ namespace Content.Server.Atmos.EntitySystems
SubscribeLocalEvent<IgniteOnHeatDamageComponent, DamageChangedEvent>(OnDamageChanged); SubscribeLocalEvent<IgniteOnHeatDamageComponent, DamageChangedEvent>(OnDamageChanged);
} }
private void OnExtinguishEvent(Entity<FlammableComponent> ent, ref ExtinguishEvent args)
{
// You know I'm really not sure if having AdjustFireStacks *after* Extinguish,
// but I'm just moving this code, not questioning it.
Extinguish(ent, ent.Comp);
AdjustFireStacks(ent, args.FireStacksAdjustment, ent.Comp);
}
private void OnMeleeHit(EntityUid uid, IgniteOnMeleeHitComponent component, MeleeHitEvent args) private void OnMeleeHit(EntityUid uid, IgniteOnMeleeHitComponent component, MeleeHitEvent args)
{ {
foreach (var entity in args.HitEntities) foreach (var entity in args.HitEntities)
@@ -315,6 +325,9 @@ namespace Content.Server.Atmos.EntitySystems
_ignitionSourceSystem.SetIgnited(uid, false); _ignitionSourceSystem.SetIgnited(uid, false);
var extinguished = new ExtinguishedEvent();
RaiseLocalEvent(uid, ref extinguished);
UpdateAppearance(uid, flammable); UpdateAppearance(uid, flammable);
} }
@@ -336,6 +349,9 @@ namespace Content.Server.Atmos.EntitySystems
else else
_adminLogger.Add(LogType.Flammable, $"{ToPrettyString(uid):target} set on fire by {ToPrettyString(ignitionSource):actor}"); _adminLogger.Add(LogType.Flammable, $"{ToPrettyString(uid):target} set on fire by {ToPrettyString(ignitionSource):actor}");
flammable.OnFire = true; flammable.OnFire = true;
var extinguished = new IgnitedEvent();
RaiseLocalEvent(uid, ref extinguished);
} }
UpdateAppearance(uid, flammable); UpdateAppearance(uid, flammable);

View File

@@ -1,5 +1,4 @@
using Content.Server.Atmos.Components; using Content.Shared.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -20,17 +19,17 @@ namespace Content.Server.EntityEffects.Effects
public override void Effect(EntityEffectBaseArgs args) public override void Effect(EntityEffectBaseArgs args)
{ {
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable)) return; var ev = new ExtinguishEvent
{
FireStacksAdjustment = FireStacksAdjustment,
};
var flammableSystem = args.EntityManager.System<FlammableSystem>();
flammableSystem.Extinguish(args.TargetEntity, flammable);
if (args is EntityEffectReagentArgs reagentArgs) if (args is EntityEffectReagentArgs reagentArgs)
{ {
flammableSystem.AdjustFireStacks(reagentArgs.TargetEntity, FireStacksAdjustment * (float) reagentArgs.Quantity, flammable); ev.FireStacksAdjustment *= (float)reagentArgs.Quantity;
} else
{
flammableSystem.AdjustFireStacks(args.TargetEntity, FireStacksAdjustment, flammable);
} }
args.EntityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ref ev);
} }
} }
} }

View File

@@ -17,6 +17,7 @@ using Content.Shared.Temperature;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using System.Linq; using System.Linq;
using Content.Shared.Atmos;
namespace Content.Server.Nutrition.EntitySystems namespace Content.Server.Nutrition.EntitySystems
{ {
@@ -48,12 +49,19 @@ namespace Content.Server.Nutrition.EntitySystems
SubscribeLocalEvent<SmokableComponent, IsHotEvent>(OnSmokableIsHotEvent); SubscribeLocalEvent<SmokableComponent, IsHotEvent>(OnSmokableIsHotEvent);
SubscribeLocalEvent<SmokableComponent, ComponentShutdown>(OnSmokableShutdownEvent); SubscribeLocalEvent<SmokableComponent, ComponentShutdown>(OnSmokableShutdownEvent);
SubscribeLocalEvent<SmokableComponent, GotEquippedEvent>(OnSmokeableEquipEvent); SubscribeLocalEvent<SmokableComponent, GotEquippedEvent>(OnSmokeableEquipEvent);
Subs.SubscribeWithRelay<SmokableComponent, ExtinguishEvent>(OnExtinguishEvent);
InitializeCigars(); InitializeCigars();
InitializePipes(); InitializePipes();
InitializeVapes(); InitializeVapes();
} }
private void OnExtinguishEvent(Entity<SmokableComponent> ent, ref ExtinguishEvent args)
{
if (ent.Comp.State == SmokableState.Lit)
SetSmokableState(ent, SmokableState.Burnt, ent);
}
public void SetSmokableState(EntityUid uid, SmokableState state, SmokableComponent? smokable = null, public void SetSmokableState(EntityUid uid, SmokableState state, SmokableComponent? smokable = null,
AppearanceComponent? appearance = null, ClothingComponent? clothing = null) AppearanceComponent? appearance = null, ClothingComponent? clothing = null)
{ {
@@ -74,9 +82,19 @@ namespace Content.Server.Nutrition.EntitySystems
_items.SetHeldPrefix(uid, newState); _items.SetHeldPrefix(uid, newState);
if (state == SmokableState.Lit) if (state == SmokableState.Lit)
{
var igniteEvent = new IgnitedEvent();
RaiseLocalEvent(uid, ref igniteEvent);
_active.Add(uid); _active.Add(uid);
}
else else
{
var igniteEvent = new ExtinguishedEvent();
RaiseLocalEvent(uid, ref igniteEvent);
_active.Remove(uid); _active.Remove(uid);
}
} }
private void OnSmokableIsHotEvent(Entity<SmokableComponent> entity, ref IsHotEvent args) private void OnSmokableIsHotEvent(Entity<SmokableComponent> entity, ref IsHotEvent args)

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Atmos.Components;
/// <summary>
/// Makes entities with extinguishing behavior automatically enable/disable <see cref="CollisionWakeComponent"/>,
/// so they can be extinguished with fire extinguishers.
/// </summary>
[RegisterComponent]
[NetworkedComponent]
public sealed partial class ExtinguishableSetCollisionWakeComponent : Component;

View File

@@ -0,0 +1,30 @@
using Content.Shared.Atmos.Components;
namespace Content.Shared.Atmos.EntitySystems;
/// <summary>
/// Implements <see cref="ExtinguishableSetCollisionWakeComponent"/>.
/// </summary>
public sealed class ExtinguishableSetCollisionWakeSystem : EntitySystem
{
[Dependency]
private readonly CollisionWakeSystem _collisionWake = null!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExtinguishableSetCollisionWakeComponent, ExtinguishedEvent>(HandleExtinguished);
SubscribeLocalEvent<ExtinguishableSetCollisionWakeComponent, IgnitedEvent>(HandleIgnited);
}
private void HandleExtinguished(Entity<ExtinguishableSetCollisionWakeComponent> ent, ref ExtinguishedEvent args)
{
_collisionWake.SetEnabled(ent, true);
}
private void HandleIgnited(Entity<ExtinguishableSetCollisionWakeComponent> ent, ref IgnitedEvent args)
{
_collisionWake.SetEnabled(ent, false);
}
}

View File

@@ -0,0 +1,42 @@
using Content.Shared.Inventory;
using Content.Shared.Nutrition.Components;
namespace Content.Shared.Atmos;
// NOTE: These components are currently not raised on the client, only on the server.
/// <summary>
/// An entity has had an existing effect applied to it.
/// </summary>
/// <remarks>
/// This does not necessarily mean the effect is strong enough to fully extinguish the entity in one go.
/// </remarks>
[ByRefEvent]
public struct ExtinguishEvent : IInventoryRelayEvent
{
/// <summary>
/// Amount of firestacks changed. Should be a negative number.
/// </summary>
public float FireStacksAdjustment;
SlotFlags IInventoryRelayEvent.TargetSlots => SlotFlags.WITHOUT_POCKET;
}
/// <summary>
/// A flammable entity has been extinguished.
/// </summary>
/// <remarks>
/// This can occur on both <c>Flammable</c> entities as well as <see cref="SmokableComponent"/>.
/// </remarks>
/// <seealso cref="ExtinguishEvent"/>
[ByRefEvent]
public struct ExtinguishedEvent;
/// <summary>
/// A flammable entity has been ignited.
/// </summary>
/// <remarks>
/// This can occur on both <c>Flammable</c> entities as well as <see cref="SmokableComponent"/>.
/// </remarks>
[ByRefEvent]
public struct IgnitedEvent;

View File

@@ -1,3 +1,4 @@
using Content.Shared.Atmos;
using Content.Shared.Camera; using Content.Shared.Camera;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
@@ -11,14 +12,31 @@ public abstract partial class SharedHandsSystem
SubscribeLocalEvent<HandsComponent, GetEyeOffsetRelayedEvent>(RelayEvent); SubscribeLocalEvent<HandsComponent, GetEyeOffsetRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, GetEyePvsScaleRelayedEvent>(RelayEvent); SubscribeLocalEvent<HandsComponent, GetEyePvsScaleRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, RefreshMovementSpeedModifiersEvent>(RelayEvent); SubscribeLocalEvent<HandsComponent, RefreshMovementSpeedModifiersEvent>(RelayEvent);
// By-ref events.
SubscribeLocalEvent<HandsComponent, ExtinguishEvent>(RefRelayEvent);
} }
private void RelayEvent<T>(Entity<HandsComponent> entity, ref T args) where T : EntityEventArgs private void RelayEvent<T>(Entity<HandsComponent> entity, ref T args) where T : EntityEventArgs
{
CoreRelayEvent(entity, ref args);
}
private void RefRelayEvent<T>(Entity<HandsComponent> entity, ref T args)
{
var ev = CoreRelayEvent(entity, ref args);
args = ev.Args;
}
private HeldRelayedEvent<T> CoreRelayEvent<T>(Entity<HandsComponent> entity, ref T args)
{ {
var ev = new HeldRelayedEvent<T>(args); var ev = new HeldRelayedEvent<T>(args);
foreach (var held in EnumerateHeld(entity, entity.Comp)) foreach (var held in EnumerateHeld(entity, entity.Comp))
{ {
RaiseLocalEvent(held, ref ev); RaiseLocalEvent(held, ref ev);
} }
return ev;
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Shared.Armor; using Content.Shared.Armor;
using Content.Shared.Atmos;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Hypospray.Events; using Content.Shared.Chemistry.Hypospray.Events;
@@ -49,6 +50,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, GetSpeedModifierContactCapEvent>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetSpeedModifierContactCapEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetSlowedOverSlipperyModifierEvent>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetSlowedOverSlipperyModifierEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ModifySlowOnDamageSpeedEvent>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, ModifySlowOnDamageSpeedEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ExtinguishEvent>(RefRelayInventoryEvent);
// Eye/vision events // Eye/vision events
SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent);

View File

@@ -0,0 +1,123 @@
using Content.Shared.Hands;
namespace Content.Shared.Inventory;
/// <summary>
/// Helper functions for subscribing to component events that are also relayed via hands/inventory.
/// </summary>
public static class RelaySubscriptionHelpers
{
/// <summary>
/// Subscribe to an event, along with different relayed event wrappers, in one call.
/// </summary>
/// <param name="subs">Subscriptions for the entity system we're subscribing on.</param>
/// <param name="handler">The event handler to be called for the event.</param>
/// <param name="baseEvent">Whether to subscribe the base event type.</param>
/// <param name="inventory">Whether to subscribe for <see cref="T:Content.Shared.Inventory.InventoryRelayedEvent`1"/>.</param>
/// <param name="held">Whether to subscribe for <see cref="T:Content.Shared.Hands.HeldRelayedEvent`1"/>.</param>
/// <seealso cref="M:Robust.Shared.GameObjects.EntitySystem.SubscribeLocalEvent``2(Robust.Shared.GameObjects.EntityEventRefHandler{``0,``1},System.Type[],System.Type[])"/>
public static void SubscribeWithRelay<TComp, TEvent>(
this EntitySystem.Subscriptions subs,
EntityEventRefHandler<TComp, TEvent> handler,
bool baseEvent = true,
bool inventory = true,
bool held = true)
where TEvent : notnull
where TComp : IComponent
{
if (baseEvent)
subs.SubscribeLocalEvent(handler);
if (inventory)
{
subs.SubscribeLocalEvent((Entity<TComp> ent, ref InventoryRelayedEvent<TEvent> ev) =>
{
handler(ent, ref ev.Args);
});
}
if (held)
{
subs.SubscribeLocalEvent((Entity<TComp> ent, ref HeldRelayedEvent<TEvent> ev) =>
{
handler(ent, ref ev.Args);
});
}
}
/// <summary>
/// Subscribe to an event, along with different relayed event wrappers, in one call.
/// </summary>
/// <param name="subs">Subscriptions for the entity system we're subscribing on.</param>
/// <param name="handler">The event handler to be called for the event.</param>
/// <param name="baseEvent">Whether to subscribe the base event type.</param>
/// <param name="inventory">Whether to subscribe for <see cref="T:Content.Shared.Inventory.InventoryRelayedEvent`1"/>.</param>
/// <param name="held">Whether to subscribe for <see cref="T:Content.Shared.Hands.HeldRelayedEvent`1"/>.</param>
/// <seealso cref="M:Robust.Shared.GameObjects.EntitySystem.SubscribeLocalEvent``2(Robust.Shared.GameObjects.ComponentEventHandler{``0,``1},System.Type[],System.Type[])"/>
public static void SubscribeWithRelay<TComp, TEvent>(
this EntitySystem.Subscriptions subs,
ComponentEventHandler<TComp, TEvent> handler,
bool baseEvent = true,
bool inventory = true,
bool held = true)
where TEvent : notnull
where TComp : IComponent
{
if (baseEvent)
subs.SubscribeLocalEvent(handler);
if (inventory)
{
subs.SubscribeLocalEvent((EntityUid uid, TComp component, InventoryRelayedEvent<TEvent> args) =>
{
handler(uid, component, args.Args);
});
}
if (held)
{
subs.SubscribeLocalEvent((EntityUid uid, TComp component, HeldRelayedEvent<TEvent> args) =>
{
handler(uid, component, args.Args);
});
}
}
/// <summary>
/// Subscribe to an event, along with different relayed event wrappers, in one call.
/// </summary>
/// <param name="subs">Subscriptions for the entity system we're subscribing on.</param>
/// <param name="handler">The event handler to be called for the event.</param>
/// <param name="baseEvent">Whether to subscribe the base event type.</param>
/// <param name="inventory">Whether to subscribe for <see cref="T:Content.Shared.Inventory.InventoryRelayedEvent`1"/>.</param>
/// <param name="held">Whether to subscribe for <see cref="T:Content.Shared.Hands.HeldRelayedEvent`1"/>.</param>
/// <seealso cref="M:Robust.Shared.GameObjects.EntitySystem.SubscribeLocalEvent``2(Robust.Shared.GameObjects.ComponentEventRefHandler{``0,``1},System.Type[],System.Type[])"/>
public static void SubscribeWithRelay<TComp, TEvent>(
this EntitySystem.Subscriptions subs,
ComponentEventRefHandler<TComp, TEvent> handler,
bool baseEvent = true,
bool inventory = true,
bool held = true)
where TEvent : notnull
where TComp : IComponent
{
if (baseEvent)
subs.SubscribeLocalEvent(handler);
if (inventory)
{
subs.SubscribeLocalEvent((EntityUid uid, TComp component, ref InventoryRelayedEvent<TEvent> args) =>
{
handler(uid, component, ref args.Args);
});
}
if (held)
{
subs.SubscribeLocalEvent((EntityUid uid, TComp component, ref HeldRelayedEvent<TEvent> args) =>
{
handler(uid, component, ref args.Args);
});
}
}
}

View File

@@ -4,6 +4,10 @@
parent: BaseItem parent: BaseItem
abstract: true abstract: true
components: components:
- type: Reactive
groups:
Extinguish: [ Touch ]
- type: ExtinguishableSetCollisionWake
- type: Smokable - type: Smokable
- type: Sprite - type: Sprite
- type: Appearance - type: Appearance

View File

@@ -27,6 +27,7 @@
variation: 0.05 variation: 0.05
volume: 10 volume: 10
- type: UseDelay - type: UseDelay
- type: ExtinguishableSetCollisionWake
- type: Flammable - type: Flammable
fireSpread: false fireSpread: false
canResistFire: false canResistFire: false

View File

@@ -130,6 +130,8 @@
mask: mask:
- FullTileMask - FullTileMask
- Opaque - Opaque
layer:
- ItemMask
- type: Appearance - type: Appearance
- type: VaporVisuals - type: VaporVisuals