diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index fb716d0ff39..0125c0a26bd 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -23,6 +23,7 @@ using Content.Shared.Toggleable; using Content.Shared.Weapons.Melee.Events; using Content.Shared.FixedPoint; +using Content.Shared.Hands; using Robust.Server.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -75,6 +76,7 @@ public override void Initialize() SubscribeLocalEvent(OnTileFire); SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnResistFireAlert); + Subs.SubscribeWithRelay(OnExtinguishEvent); SubscribeLocalEvent(IgniteOnCollide); SubscribeLocalEvent(OnIgniteLand); @@ -88,6 +90,14 @@ public override void Initialize() SubscribeLocalEvent(OnDamageChanged); } + private void OnExtinguishEvent(Entity 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) { foreach (var entity in args.HitEntities) @@ -331,6 +341,9 @@ public void Extinguish(EntityUid uid, FlammableComponent? flammable = null) _ignitionSourceSystem.SetIgnited(uid, false); + var extinguished = new ExtinguishedEvent(); + RaiseLocalEvent(uid, ref extinguished); + UpdateAppearance(uid, flammable); } @@ -361,6 +374,9 @@ public void Ignite(EntityUid uid, EntityUid ignitionSource, FlammableComponent? else _adminLogger.Add(LogType.Flammable, $"{ToPrettyString(uid):target} set on fire by {ToPrettyString(ignitionSource):actor}"); flammable.OnFire = true; + + var extinguished = new IgnitedEvent(); + RaiseLocalEvent(uid, ref extinguished); } UpdateAppearance(uid, flammable); diff --git a/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs index 6d7e7c2fcd8..d36ac9a576f 100644 --- a/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs +++ b/Content.Server/EntityEffects/Effects/ExtinguishReaction.cs @@ -1,5 +1,4 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -20,17 +19,17 @@ public sealed partial class ExtinguishReaction : EntityEffect 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.Extinguish(args.TargetEntity, flammable); if (args is EntityEffectReagentArgs reagentArgs) { - flammableSystem.AdjustFireStacks(reagentArgs.TargetEntity, FireStacksAdjustment * (float) reagentArgs.Quantity, flammable); - } else - { - flammableSystem.AdjustFireStacks(args.TargetEntity, FireStacksAdjustment, flammable); + ev.FireStacksAdjustment *= (float)reagentArgs.Quantity; } + + args.EntityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ref ev); } } } diff --git a/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs index a6cdda247c3..9c939433e39 100644 --- a/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs +++ b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs @@ -1,4 +1,5 @@ -using Content.Shared.Mobs; +using System.Threading; +using Content.Shared.Mobs; namespace Content.Server.Explosion.Components; @@ -21,4 +22,12 @@ public sealed partial class TriggerOnMobstateChangeComponent : Component [ViewVariables] [DataField("preventSuicide")] public bool PreventSuicide = false; + + // the timer cancel token + [ViewVariables] + public CancellationTokenSource RattleCancelToken = new(); + + // The delay before the implant sends the message again + [DataField] + public TimeSpan RattleRefireDelay = TimeSpan.FromMinutes(20); } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs index ccd2a6e3df0..69faa561ba4 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs @@ -1,8 +1,14 @@ -using Content.Server.Explosion.Components; +using System.Threading; +using Content.Server.Explosion.Components; using Content.Shared.Explosion.Components; using Content.Shared.Implants; using Content.Shared.Interaction.Events; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Verbs; +using Robust.Shared.Timing; namespace Content.Server.Explosion.EntitySystems; @@ -15,27 +21,95 @@ private void InitializeMobstate() SubscribeLocalEvent>(OnSuicideRelay); SubscribeLocalEvent>(OnMobStateRelay); + SubscribeLocalEvent>(OnFtlArriveRelay); } private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent component, MobStateChangedEvent args) { + component.RattleCancelToken.Cancel(); + component.RattleCancelToken = new CancellationTokenSource(); if (!component.MobState.Contains(args.NewMobState)) return; + TryRunTrigger( + uid, + component, + args.Target, + args.NewMobState, + args.Origin); + } + + private void TryRunTrigger( + EntityUid uid, + TriggerOnMobstateChangeComponent component, + EntityUid changedStateMobUid, + MobState coolState, + EntityUid? stateChangerUid = null, + bool retry = false) + { + //This chains Mobstate Changed triggers with OnUseTimerTrigger if they have it //Very useful for things that require a mobstate change and a timer if (TryComp(uid, out var timerTrigger)) { HandleTimerTrigger( uid, - args.Origin, + stateChangerUid, timerTrigger.Delay, timerTrigger.BeepInterval, timerTrigger.InitialBeepDelay, timerTrigger.BeepSound); } else - Trigger(uid); + { + Dictionary extraData = new() + { + { "isRetry", retry } + }; + Trigger(uid, extras: extraData); + } + + // but only repeat if their mind has a people behind it + if (!TryComp(changedStateMobUid, out var mindContainer)) + return; + var mind = CompOrNull(mindContainer.Mind); + var hasUserId = mind?.UserId; + if (hasUserId == null) + return; + + // then do it AGAIN + component.RattleCancelToken.Cancel(); + component.RattleCancelToken = new CancellationTokenSource(); + Robust.Shared.Timing.Timer.Spawn(component.RattleRefireDelay, () => CheckAndTryRefire(uid, component, changedStateMobUid), component.RattleCancelToken.Token); + } + + /// + /// Check if the trigger can be retriggered and does so if possible + /// + private void CheckAndTryRefire( + EntityUid uid, + TriggerOnMobstateChangeComponent component, + EntityUid changedStateMobUid) + { + if (!Exists(uid) + || !Exists(changedStateMobUid)) + return; + if (Deleted(uid) + || Deleted(changedStateMobUid)) + return; + if (!HasComp(changedStateMobUid)) + return; + var stat = Comp(changedStateMobUid).CurrentState; + if (component.MobState.Contains(stat)) + { + TryRunTrigger( + uid, + component, + changedStateMobUid, + stat, + null, + true); + } } /// @@ -61,6 +135,24 @@ private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent comp private void OnMobStateRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent args) { - OnMobStateChanged(uid, component, args.Event); + OnMobStateChanged( + uid, + component, + args.Event); + } + + /// + /// When ftl arrives, and they are fucked, do the needful + /// + private void OnFtlArriveRelay(EntityUid uid, + TriggerOnMobstateChangeComponent component, + ImplantRelayEvent args) + { + TryRunTrigger( + uid, + component, + args.Event.Implanted, + args.Event.CurrentState, + null); } } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index d3897653493..7db1a2b7797 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -1,4 +1,6 @@ using Content.Server._Mono.Planets; +using System.Linq; +using System.Threading; using Content.Server.Administration.Logs; using Content.Server.Body.Systems; using Content.Server.Explosion.Components; @@ -35,6 +37,8 @@ using Content.Shared.Humanoid; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Utility; using Content.Shared.Body.Components; // Frontier: Gib organs using Content.Shared.Projectiles; // Frontier: embed triggers using Content.Shared.Mind; @@ -51,14 +55,26 @@ public sealed class TriggerEvent : HandledEntityEventArgs { public EntityUid Triggered { get; } public EntityUid? User { get; } + public Dictionary Extras { get; } = new(); public TriggerEvent(EntityUid triggered, EntityUid? user = null) { Triggered = triggered; User = user; } + + public void AddExtra(string extra, object value) + { + Extras[extra] = value; + } } + /// + /// Raised before a trigger is activated. + /// + [ByRefEvent] + public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false); + /// /// Raised when timer trigger becomes active. /// @@ -250,8 +266,8 @@ private void HandleRattleTrigger(EntityUid uid, RattleComponent component, Trigg // Gets location of the implant var ownerXform = Transform(uid); var pos = ownerXform.MapPosition; - var x = (int) pos.X; - var y = (int) pos.Y; + var x = (int)pos.X; + var y = (int)pos.Y; var posText = $"({x}, {y})"; // Frontier: Gets station location of the implant @@ -367,9 +383,16 @@ private void OnRepeatInit(Entity ent, ref MapInitEven ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay; } - public bool Trigger(EntityUid trigger, EntityUid? user = null) + public bool Trigger(EntityUid trigger, EntityUid? user = null, Dictionary? extras = null) { var triggerEvent = new TriggerEvent(trigger, user); + if (extras != null) + { + foreach (var (key, value) in extras) + { + triggerEvent.AddExtra(key, value); + } + } EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true); return triggerEvent.Handled; } diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index 0d637139d82..d2074a3d82d 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -17,6 +17,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Containers; using System.Linq; +using Content.Shared.Atmos; namespace Content.Server.Nutrition.EntitySystems { @@ -48,12 +49,19 @@ public override void Initialize() SubscribeLocalEvent(OnSmokableIsHotEvent); SubscribeLocalEvent(OnSmokableShutdownEvent); SubscribeLocalEvent(OnSmokeableEquipEvent); + Subs.SubscribeWithRelay(OnExtinguishEvent); InitializeCigars(); InitializePipes(); InitializeVapes(); } + private void OnExtinguishEvent(Entity 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, AppearanceComponent? appearance = null, ClothingComponent? clothing = null) { @@ -74,9 +82,19 @@ public void SetSmokableState(EntityUid uid, SmokableState state, SmokableCompone _items.SetHeldPrefix(uid, newState); if (state == SmokableState.Lit) + { + var igniteEvent = new IgnitedEvent(); + RaiseLocalEvent(uid, ref igniteEvent); + _active.Add(uid); + } else + { + var igniteEvent = new ExtinguishedEvent(); + RaiseLocalEvent(uid, ref igniteEvent); + _active.Remove(uid); + } } private void OnSmokableIsHotEvent(Entity entity, ref IsHotEvent args) diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs index 819eecaac6b..757abd4ed00 100644 --- a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs +++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs @@ -71,4 +71,10 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition [ViewVariables(VVAccess.ReadWrite), DataField("rewards", customTypeSerializer: typeof(PrototypeIdListSerializer))] public List Rewards = default!; // End Frontier: expedition difficulty and rewards + + /// + /// next time to check for autoabort + /// + [ViewVariables(VVAccess.ReadWrite), DataField] + public TimeSpan NextAutoAbortCheck = TimeSpan.Zero; } diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 6557898788c..8d3f6d37990 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -15,8 +15,22 @@ using Content.Shared.Localizations; using Robust.Shared.Map.Components; using Robust.Shared.Player; +using Content.Server.Body.Components; +using Content.Server.Buckle.Systems; +using Content.Server.Temperature.Components; +using Content.Server.Temperature.Systems; +using Content.Shared.Atmos; +using Content.Shared.Buckle.Components; +using Content.Shared.Damage; +using Content.Shared.Mind.Components; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Enums; // Frontier using Robust.Shared.Utility; -using Content.Shared.Coordinates; +using Content.Shared._Triad; +using Content.Server.Warps; +using Content.Shared.Inventory; +using Content.Shared.Damage.Systems; namespace Content.Server.Salvage; @@ -27,7 +41,14 @@ public sealed partial class SalvageSystem */ [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; // Frontier + [Dependency] private readonly BuckleSystem _buckle = default!; + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly TemperatureSystem _temperature = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + private void InitializeRunner() { SubscribeLocalEvent(OnFTLRequest); @@ -186,6 +207,8 @@ private void UpdateRunner() var remaining = comp.EndTime - _timing.CurTime; var audioLength = _audio.GetAudioLength(comp.SelectedSong); + AbortIfWiped(uid, comp); // Coyote + if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45)) { comp.Stage = ExpeditionStage.FinalCountdown; @@ -237,15 +260,42 @@ private void UpdateRunner() continue; } + var shuttleGrid = shuttleXform.GridUid; + DestinationPriority? deadLoserDestinations = null; + // Need to check if the shuttle have any FTLComponent or not. + if (shuttleGrid != null && !HasComp(shuttleGrid)) + { + var mobQuery = EntityQueryEnumerator(); + while (mobQuery.MoveNext( + out var mobUid, + out var mindC, + out var mobXform)) + { + if (mobXform.MapUid != uid) + continue; + if (mobXform.GridUid == shuttleGrid) + continue; // they're already on the shuttle + // only count creatures that have at one point had a player controlling them + if (!mindC.HasMind) + continue; + // move them to the shuttle + deadLoserDestinations ??= GetDeadPersonDestinations(shuttleGrid.Value); + RescuePlayer( + mobUid, + deadLoserDestinations, + shuttleGrid.Value); + } + } + // Destination generator parameters (move to CVAR?) - int numRetries = 20; // Maximum number of retries - float minDistance = 200f; // Minimum distance from another object, in meters - float minRange = 750f; // Minimum distance from sector centre, in meters - float maxRange = 3500f; // Maximum distance from sector centre, in meters + var numRetries = 20; // Maximum number of retries + var minDistance = 200f; // Minimum distance from another object, in meters + var minRange = 750f; // Minimum distance from sector centre, in meters + var maxRange = 3500f; // Maximum distance from sector centre, in meters // Get a list of all grid positions on the destination map List gridCoords = new(); - var gridQuery = EntityManager.AllEntityQueryEnumerator(); + var gridQuery = AllEntityQuery(); while (gridQuery.MoveNext(out var _, out _, out var xform)) { if (xform.MapID == mapId) @@ -271,7 +321,6 @@ private void UpdateRunner() // No good position yet, pick another random position. dropLocation = _random.NextVector2(minRange, maxRange); } - _shuttle.FTLToCoordinates(shuttleUid, shuttle, new EntityCoordinates(mapUid.Value, dropLocation), 0f, 5.5f, 50f); // End Frontier: try to find a potential destination for ship that doesn't collide with other grids. } @@ -358,5 +407,277 @@ private void UpdateRunner() Announce(uid, Loc.GetString("salvage-expedition-completed")); } } + // End Frontier: mission-specific logic + } + + /// + /// Takes a mob, and puts them onto this shuttle. + /// + private void RescuePlayer( + EntityUid mobUid, + DestinationPriority possibleDestinations, + EntityUid shuttleGrid) + { + PrepareRescue(mobUid); + // unbuckle them if they are buckled + _buckle.TryUnbuckle(mobUid, null); + // try beds first + foreach (var bedUid in possibleDestinations.Beds) + { + if (TryTeleportToStrap(mobUid, bedUid)) + return; + } + // then chairs + foreach (var chairUid in possibleDestinations.Chairs) + { + if (TryTeleportToStrap(mobUid, chairUid)) + return; + } + // then consoles + foreach (var consoleUid in possibleDestinations.Consoles) + { + var consoleXform = Transform(consoleUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, consoleXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return; + } + // then fallback + foreach (var fallbackUid in possibleDestinations.Fallback) + { + var fallbackXform = Transform(fallbackUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, fallbackXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return; + } + } + + /// + /// Gets a list of possible destinations for dead/dying crew to be rescued to. + /// Tries to find a location based on a list of priorities. + /// HERES THE PRIORITIES: + /// 2: Beds with no mobs in them. + /// 3: Chairs with no mobs in them. + /// 4: I dunno the console I guess + /// + private DestinationPriority GetDeadPersonDestinations(EntityUid shuttleGrid) + { + DestinationPriority destinations = new(); + // first, find the exped consoles on the grid + var destQuery = EntityQueryEnumerator(); + while (destQuery.MoveNext( + out var uid, + out var _, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, DestinationType.Console); + } + // then, all beds / chairs (theyre both strap components) + var strapQuery = EntityQueryEnumerator(); + while (strapQuery.MoveNext( + out var uid, + out var strap, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, strap.Position == StrapPosition.Stand ? DestinationType.Chair : DestinationType.Bed); + } + // then some fallback stuff, find the warp point + // worst case, we just teleport them to the center of the grid. hope its not in a wall!! + var warpQuery = EntityQueryEnumerator(); + while (warpQuery.MoveNext( + out var uid, + out var _, + out var xform)) + { + if (xform.GridUid != shuttleGrid) + continue; + destinations.Add(uid, DestinationType.Fallback); + } + return destinations; + } + + /// + /// Beats the heck out of the dork if they arent dead + /// Then extinguishes them and caps their Heat to 300ish if above that. + /// + private void PrepareRescue(EntityUid mobUid) + { + if (_mobState.IsAlive(mobUid)) + { + // Force stamcrit when rescue + _stamina.TakeStaminaDamage(mobUid, 200); + _inventorySystem.TryUnequip(mobUid, "suitStorage", true, true, false); + } + if (!_mobState.IsAlive(mobUid)) + { + // Force strip backpack, outerClothing, and suitStorage if they're not alive + _inventorySystem.TryUnequip(mobUid, "back", true, true, false); + _inventorySystem.TryUnequip(mobUid, "outerClothing", true, true, false); + _inventorySystem.TryUnequip(mobUid, "suitStorage", true, true, false); + } + + var ev = new ExtinguishEvent + { + FireStacksAdjustment = 1000, + }; + if (!TryComp(mobUid, out var comp)) + return; + if (TryComp( + mobUid, + out var regulator)) // Frontier: Look for normal body temperature and use it + { + _temperature.ForceChangeTemperature( + mobUid, + regulator.NormalBodyTemperature, + comp); + } + else + { + _temperature.ForceChangeTemperature( + mobUid, + Atmospherics.T20C, + comp); + } + } + + /// + /// Tries to teleport the mob to the strap and buckle them in. + /// Returns true on success. + /// + private bool TryTeleportToStrap(EntityUid mobUid, EntityUid strapUid) + { + if (!TryComp(mobUid, out var buckle)) + return false; + if (!TryComp(strapUid, out var strap)) + return false; + if (strap.BuckledEntities.Count > 0) + return false; // already occupied + var strapXform = Transform(strapUid); + var mobXform = Transform(mobUid); + _transform.SetCoordinates(mobUid, strapXform.Coordinates); + _transform.AttachToGridOrMap(mobUid, mobXform); + return _buckle.TryBuckle( + mobUid, + null, + strapUid); + } + + // class that holds a set of destinations with a priority + private sealed class DestinationPriority + { + public List Beds = new(); + public List Chairs = new(); + public List Consoles = new(); + public List Fallback = new(); + public void Add(EntityUid uid, DestinationType type) + { + switch (type) + { + case DestinationType.Bed: + Beds.Add(uid); + break; + case DestinationType.Chair: + Chairs.Add(uid); + break; + case DestinationType.Console: + Consoles.Add(uid); + break; + default: + case DestinationType.Fallback: + Fallback.Add(uid); + break; + } + } + } + + // enum for destination types + private enum DestinationType + { + Bed, + Chair, + Console, + Fallback, + } + + /// + /// Checks if everyone on the map worth caring about is dead, and aborts the expedition if so. + /// Honestly, as long as one person is not in crit and not SSD, we consider the expedition salvageable. + /// + private void AbortIfWiped(EntityUid mapUid, SalvageExpeditionComponent component) + { + // give it a 30 second grade after first check to avoid instant aborts + if (component.NextAutoAbortCheck == TimeSpan.Zero) + { + component.NextAutoAbortCheck = _timing.CurTime + TimeSpan.FromSeconds(30); + return; + } + // its an entity query and idk how expensive it is, so, cooldown + if (_timing.CurTime < component.NextAutoAbortCheck) + return; + component.NextAutoAbortCheck = _timing.CurTime + TimeSpan.FromSeconds(15); + + // okay first look for aghosts, whatever + var aghostQuery = + EntityQueryEnumerator(); + while (aghostQuery.MoveNext( + out var _, + out _, + out var xform)) + { + if (xform.MapUid == mapUid) + return; // aghost found, dont abort + } + + var query = + EntityQueryEnumerator< + HumanoidAppearanceComponent, + MindContainerComponent, + MobStateComponent, + TransformComponent>(); + // prevent abort if: + // - aghosts are present + // - anyone is alive AND connected + while (query.MoveNext( + out var uid, + out _, + out var mindC, + out var mobState, + out var xform)) + { + if (xform.MapUid != mapUid) + continue; + // unidentified humans (loot) dont count + if (!mindC.HasMind) + continue; + // if anyone is alive and not in crit, we are good + if (_mobState.IsAlive(uid, mobState)) + { + // okay weve got something alive, is their session? + _players.TryGetSessionByEntity(uid, out var session); + // if no session, check if they are SSD + if (session == null) + continue; + if (session.Status == SessionStatus.Disconnected) + continue; + return; // alive and connected player found, expedition is salvageable + } + } + // everyone is dead or ssd, abort the expedition + const int departTime = 30; + Announce(mapUid, Loc.GetString("salvage-expedition-abort-wipe", ("departTime", departTime))); + component.NextAutoAbortCheck = TimeSpan.FromDays(1); // prevent further checks + var newEndTime = _timing.CurTime + TimeSpan.FromSeconds(departTime); + + if (component.EndTime <= newEndTime) + return; + + component.Stage = ExpeditionStage.FinalCountdown; + component.EndTime = newEndTime; + } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index ed8b559de28..7e9ec84141f 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -9,7 +9,10 @@ using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Ghost; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; using Content.Shared.Maps; +using Content.Shared.Mobs.Components; using Content.Shared.Parallax; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; @@ -899,6 +902,23 @@ private void UpdateFTLArriving(Entity entity) Enable(uid, component: body, shuttle: entity.Comp2); } } + + // COYOTE: when the shuttle arrives, go through all the mobs on the grid + // and attempt to set off their deathrattle implants + var shuttleGridId = xform.GridUid; + var implantedQuery = EntityQueryEnumerator(); + while (implantedQuery.MoveNext( + out var mobUid, + out var implanted, + out var mobState, + out var mobXform)) + { + if (mobXform.GridUid != shuttleGridId) + continue; + + var deathrattleEvent = new ReTriggerRattleImplantEvent(mobUid, mobState.CurrentState); + RaiseLocalEvent(mobUid, deathrattleEvent); + } } private void UpdateFTLCooldown(Entity entity) diff --git a/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs b/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs new file mode 100644 index 00000000000..19e471f0e58 --- /dev/null +++ b/Content.Shared/Atmos/Components/ExtinguishableSetCollisionWakeComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +/// +/// Makes entities with extinguishing behavior automatically enable/disable , +/// so they can be extinguished with fire extinguishers. +/// +[RegisterComponent] +[NetworkedComponent] +public sealed partial class ExtinguishableSetCollisionWakeComponent : Component; diff --git a/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs b/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs new file mode 100644 index 00000000000..107ac5efd76 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/ExtinguishableSetCollisionWakeSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Shared.Atmos.EntitySystems; + +/// +/// Implements . +/// +public sealed class ExtinguishableSetCollisionWakeSystem : EntitySystem +{ + [Dependency] + private readonly CollisionWakeSystem _collisionWake = null!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleExtinguished); + SubscribeLocalEvent(HandleIgnited); + } + + private void HandleExtinguished(Entity ent, ref ExtinguishedEvent args) + { + _collisionWake.SetEnabled(ent, true); + } + + private void HandleIgnited(Entity ent, ref IgnitedEvent args) + { + _collisionWake.SetEnabled(ent, false); + } +} diff --git a/Content.Shared/Atmos/FireEvents.cs b/Content.Shared/Atmos/FireEvents.cs new file mode 100644 index 00000000000..4e19ef61478 --- /dev/null +++ b/Content.Shared/Atmos/FireEvents.cs @@ -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. + +/// +/// An entity has had an existing effect applied to it. +/// +/// +/// This does not necessarily mean the effect is strong enough to fully extinguish the entity in one go. +/// +[ByRefEvent] +public struct ExtinguishEvent : IInventoryRelayEvent +{ + /// + /// Amount of firestacks changed. Should be a negative number. + /// + public float FireStacksAdjustment; + + SlotFlags IInventoryRelayEvent.TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// A flammable entity has been extinguished. +/// +/// +/// This can occur on both Flammable entities as well as . +/// +/// +[ByRefEvent] +public struct ExtinguishedEvent; + +/// +/// A flammable entity has been ignited. +/// +/// +/// This can occur on both Flammable entities as well as . +/// +[ByRefEvent] +public struct IgnitedEvent; diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs index a0e02cf2e1a..742e6325010 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs @@ -1,3 +1,4 @@ +using Content.Shared.Atmos; using Content.Shared.Camera; using Content.Shared.Hands.Components; using Content.Shared.Movement.Systems; @@ -11,14 +12,31 @@ private void InitializeRelay() SubscribeLocalEvent(RelayEvent); SubscribeLocalEvent(RelayEvent); SubscribeLocalEvent(RelayEvent); + + // By-ref events. + SubscribeLocalEvent(RefRelayEvent); } private void RelayEvent(Entity entity, ref T args) where T : EntityEventArgs + { + CoreRelayEvent(entity, ref args); + } + + private void RefRelayEvent(Entity entity, ref T args) + { + var ev = CoreRelayEvent(entity, ref args); + args = ev.Args; + } + + private HeldRelayedEvent CoreRelayEvent(Entity entity, ref T args) { var ev = new HeldRelayedEvent(args); + foreach (var held in EnumerateHeld(entity, entity.Comp)) { RaiseLocalEvent(held, ref ev); } + + return ev; } } diff --git a/Content.Shared/Implants/Components/RattleComponent.cs b/Content.Shared/Implants/Components/RattleComponent.cs index b166e0e5db6..5e90c061219 100644 --- a/Content.Shared/Implants/Components/RattleComponent.cs +++ b/Content.Shared/Implants/Components/RattleComponent.cs @@ -1,4 +1,5 @@ using Content.Shared._EinsteinEngines.Language; +using System.Threading; using Content.Shared.Radio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index bb166b3c5cb..aaab8f53af8 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Mobs; using Content.Shared.Tag; +using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -30,6 +31,8 @@ public override void Initialize() SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent>(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); } private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args) @@ -217,3 +220,15 @@ public ImplantImplantedEvent(EntityUid implant, EntityUid? implanted) Implanted = implanted; } } + +/// +/// Event used to re-trigger implant events, if needed. +/// Raised on the implanted entity. +/// +public sealed class ReTriggerRattleImplantEvent( + EntityUid implanted, + MobState currentState) : EventArgs +{ + public readonly EntityUid Implanted = implanted; + public readonly MobState CurrentState = currentState; +} diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 443755581b9..9e7cb33b500 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,6 +1,7 @@ using Content.Shared._Goobstation.Flashbang; using Content.Shared._White.Overlays; using Content.Shared.Armor; +using Content.Shared.Atmos; using Content.Shared.Chat; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Hypospray.Events; @@ -57,6 +58,7 @@ public void InitializeRelay() SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); // Eye/vision events diff --git a/Content.Shared/Inventory/RelaySubscriptionHelpers.cs b/Content.Shared/Inventory/RelaySubscriptionHelpers.cs new file mode 100644 index 00000000000..e9052315394 --- /dev/null +++ b/Content.Shared/Inventory/RelaySubscriptionHelpers.cs @@ -0,0 +1,123 @@ +using Content.Shared.Hands; + +namespace Content.Shared.Inventory; + +/// +/// Helper functions for subscribing to component events that are also relayed via hands/inventory. +/// +public static class RelaySubscriptionHelpers +{ + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + EntityEventRefHandler 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 ent, ref InventoryRelayedEvent ev) => + { + handler(ent, ref ev.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((Entity ent, ref HeldRelayedEvent ev) => + { + handler(ent, ref ev.Args); + }); + } + } + + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + ComponentEventHandler 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 args) => + { + handler(uid, component, args.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, HeldRelayedEvent args) => + { + handler(uid, component, args.Args); + }); + } + } + + /// + /// Subscribe to an event, along with different relayed event wrappers, in one call. + /// + /// Subscriptions for the entity system we're subscribing on. + /// The event handler to be called for the event. + /// Whether to subscribe the base event type. + /// Whether to subscribe for . + /// Whether to subscribe for . + /// + public static void SubscribeWithRelay( + this EntitySystem.Subscriptions subs, + ComponentEventRefHandler 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 args) => + { + handler(uid, component, ref args.Args); + }); + } + + if (held) + { + subs.SubscribeLocalEvent((EntityUid uid, TComp component, ref HeldRelayedEvent args) => + { + handler(uid, component, ref args.Args); + }); + } + } +} diff --git a/Content.Shared/_Triad/AdminGhostComponent.cs b/Content.Shared/_Triad/AdminGhostComponent.cs new file mode 100644 index 00000000000..11726957046 --- /dev/null +++ b/Content.Shared/_Triad/AdminGhostComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared._Triad; + +/// +/// Determined that it's admin ghost +/// +[RegisterComponent] +public sealed partial class AdminGhostComponent : Component +{ + +} diff --git a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl index bdea86044bb..a4d8b1cbcf1 100644 --- a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl +++ b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl @@ -1,5 +1,7 @@ salvage-expedition-window-finish = Finish expedition salvage-expedition-announcement-early-finish = The expedition was completed ahead of schedule. Shuttle will depart in {$departTime} seconds. +salvage-expedition-abort-wipe = [color=Red]No life signs detected[/color]. Expedition aborted. Commencing personnel recovery. Shuttle departure in {$departTime} seconds. + salvage-expedition-shuttle-not-found = Cannot locate shuttle. salvage-expedition-not-everyone-aboard = Not all crew aboard! {CAPITALIZE(THE($target))} is still out there! salvage-expedition-ftl-cooldown = The FTL drive is still spooling down from the jump in. Wait for it to cool down before returning. @@ -51,3 +53,7 @@ salvage-air-mod-16 = 34 CO2, 34 NH3, 34 N2O salvage-air-mod-17 = 34 H2O, 34 NH3, 34 N2O salvage-air-mod-18 = 34 H2O, 34 N2O, 17 NH3, 17 CO2 salvage-air-mod-unknown = Unknown atmosphere + +salvage-expedition-difficulty-NFModerate = Moderate +salvage-expedition-difficulty-NFHazardous = Hazardous +salvage-expedition-difficulty-NFExtreme = Extreme diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index 06e1909c9f2..04296c1ae82 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -34,3 +34,6 @@ scramble-implant-activated-popup = Your appearance shifts and changes! deathrattle-implant-dead-message = {$user}{$specie} has died at {$grid}{$position}. deathrattle-implant-critical-message = {$user}{$specie} life signs critical, immediate assistance required at {$grid}{$position}. + +deathrattle-implant-dead-message-still = {$user}{$specie} is STILL dead at {$grid}{$position}! +deathrattle-implant-critical-message-still = {$user}{$specie} life signs are still critical, immediate assistance is STILL required at {$grid}{$position}. diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index f133f9743ba..6c47ea31975 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -19,6 +19,7 @@ context: "aghost" - type: Ghost canInteract: true + - type: AdminGhost - type: GhostHearing - type: Hands - type: ComplexInteraction diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml index 0d88c149e48..247468a1f16 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/base_smokeables.yml @@ -4,6 +4,10 @@ parent: BaseItem abstract: true components: + - type: Reactive + groups: + Extinguish: [ Touch ] + - type: ExtinguishableSetCollisionWake - type: Smokable - type: Sprite - type: Appearance diff --git a/Resources/Prototypes/Entities/Objects/Misc/candles.yml b/Resources/Prototypes/Entities/Objects/Misc/candles.yml index c5a4fd65ac4..5f6624c923b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/candles.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/candles.yml @@ -27,6 +27,7 @@ variation: 0.05 volume: 10 - type: UseDelay + - type: ExtinguishableSetCollisionWake - type: Flammable fireSpread: false canResistFire: false diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml index 0ce36c938f7..766acdf6a94 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml @@ -130,6 +130,8 @@ mask: - FullTileMask - Opaque + layer: + - ItemMask - type: Appearance - type: VaporVisuals